diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 2725e32176..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,27 +0,0 @@ - -**Affects Version(s):** \ - ---- - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..cfab0e3ba9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'type: bug, status: waiting-for-triage' +assignees: '' + +--- + +**In what version(s) of Spring AMQP are you seeing this issue?** + +For example: + +2.4.2 + +Between 2.3.0 and 2.4.2 + +**Describe the bug** + +A clear and concise description of what the bug is. +Do not create an issue to ask a question; see below. + +**To Reproduce** + +Steps to reproduce the behavior. + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Sample** + +A link to a GitHub repository with a [minimal, reproducible, sample](https://stackoverflow.com/help/minimal-reproducible-example). + +Reports that include a sample will take priority over reports that do not. +At times, we may require a sample, so it is good to try and include a sample up front. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..bddfd341d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Support + url: https://stackoverflow.com/questions/tagged/spring-amqp + about: Please ask and answer questions on StackOverflow with the tag spring-amqp, or use the Discussions tab above diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..faa6dff469 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'status: waiting-for-triage, type: enhancement' +assignees: '' + +--- + +**Expected Behavior** + + + +**Current Behavior** + + + +**Context** + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f232d0d7b0..eb37328f9a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,6 @@ diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 0000000000..0c4b142e9a --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..7f27837c68 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,132 @@ +version: 2 +updates: + - package-ecosystem: gradle + directory: / + schedule: + interval: weekly + day: saturday + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + - version-update:semver-minor + open-pull-requests-limit: 10 + labels: + - 'type: dependency-upgrade' + groups: + development-dependencies: + update-types: + - patch + patterns: + - com.gradle.* + - io.spring.* + - org.ajoberstar.grgit + - org.antora + - io.micrometer:micrometer-docs-generator + - com.willowtreeapps.assertk:assertk-jvm + - org.hibernate.validator:hibernate-validator + - org.apache.httpcomponents.client5:httpclient5 + - org.awaitility:awaitility + - net.ltgt.errorprone + - com.uber.nullaway* + - com.google.errorprone* + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: saturday + labels: + - 'type: task' + groups: + development-dependencies: + patterns: + - '*' + + - package-ecosystem: gradle + target-branch: 3.2.x + directory: / + schedule: + interval: weekly + day: saturday + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + - version-update:semver-minor + open-pull-requests-limit: 10 + labels: + - 'type: dependency-upgrade' + groups: + development-dependencies: + update-types: + - patch + patterns: + - com.gradle.* + - com.github.spotbugs + - io.spring.* + - org.ajoberstar.grgit + - org.antora + - io.micrometer:micrometer-docs-generator + - com.willowtreeapps.assertk:assertk-jvm + - org.hibernate.validator:hibernate-validator + - org.apache.httpcomponents.client5:httpclient5 + - org.awaitility:awaitility + + - package-ecosystem: github-actions + target-branch: 3.2.x + directory: / + schedule: + interval: weekly + day: saturday + labels: + - 'type: task' + groups: + development-dependencies: + patterns: + - '*' + + - package-ecosystem: gradle + target-branch: 3.1.x + directory: / + schedule: + interval: monthly + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + - version-update:semver-minor + open-pull-requests-limit: 10 + labels: + - 'type: dependency-upgrade' + groups: + development-dependencies: + update-types: + - patch + patterns: + - com.gradle.* + - com.github.spotbugs + - io.spring.* + - org.ajoberstar.grgit + - org.antora + - io.micrometer:micrometer-docs-generator + - com.willowtreeapps.assertk:assertk-jvm + - org.hibernate.validator:hibernate-validator + - org.apache.httpcomponents.client5:httpclient5 + - org.awaitility:awaitility + - org.xerial.snappy:snappy-java + - org.lz4:lz4-java + - com.github.luben:zstd-jni + + - package-ecosystem: github-actions + target-branch: 3.1.x + directory: / + schedule: + interval: weekly + day: saturday + labels: + - 'type: task' + groups: + development-dependencies: + patterns: + - '*' \ No newline at end of file diff --git a/.github/release-files-spec.json b/.github/release-files-spec.json deleted file mode 100644 index 4ceba9f6d9..0000000000 --- a/.github/release-files-spec.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "files": [ - { - "aql": { - "items.find": { - "$and": [ - { - "@build.name": "${buildname}", - "@build.number": "${buildnumber}", - "path": {"$match": "org*"} - }, - { - "$or": [ - { - "name": {"$match": "*.pom"} - }, - { - "name": {"$match": "*.jar"} - } - ] - } - ] - } - }, - "target": "nexus/" - } - ] -} diff --git a/.github/workflows/announce-milestone-planning.yml b/.github/workflows/announce-milestone-planning.yml new file mode 100644 index 0000000000..58ba601906 --- /dev/null +++ b/.github/workflows/announce-milestone-planning.yml @@ -0,0 +1,11 @@ +name: Announce Milestone Planning in Chat + +on: + milestone: + types: [created, edited] + +jobs: + announce-milestone-planning: + uses: spring-io/spring-github-workflows/.github/workflows/spring-announce-milestone-planning.yml@v5 + secrets: + SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/auto-cherry-pick.yml b/.github/workflows/auto-cherry-pick.yml new file mode 100644 index 0000000000..6ba14dde29 --- /dev/null +++ b/.github/workflows/auto-cherry-pick.yml @@ -0,0 +1,13 @@ +name: Auto Cherry-Pick + +on: + push: + branches: + - main + - '*.x' + +jobs: + cherry-pick-commit: + uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@v5 + secrets: + GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/backport-issue.yml b/.github/workflows/backport-issue.yml new file mode 100644 index 0000000000..71e42771d5 --- /dev/null +++ b/.github/workflows/backport-issue.yml @@ -0,0 +1,12 @@ +name: Backport Issue + +on: + push: + branches: + - '*.x' + +jobs: + backport-issue: + uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v5 + secrets: + GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/central-sync-close.yml b/.github/workflows/central-sync-close.yml deleted file mode 100644 index a00aebd4d4..0000000000 --- a/.github/workflows/central-sync-close.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Central Sync Close - -on: - workflow_dispatch: - inputs: - stagedRepositoryId: - description: "Staged repository id" - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - # Request close promotion repo - - uses: jvalkeal/nexus-sync@v0 - with: - url: ${{ secrets.OSSRH_URL }} - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - staging-repo-id: ${{ github.event.inputs.stagedRepositoryId }} - close: true diff --git a/.github/workflows/central-sync-create.yml b/.github/workflows/central-sync-create.yml deleted file mode 100644 index 2d31cdcaa7..0000000000 --- a/.github/workflows/central-sync-create.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Central Sync Create - -on: - workflow_dispatch: - inputs: - buildName: - description: "Artifactory build name" - required: true - buildNumber: - description: "Artifactory build number" - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - # to get spec file in .github - - uses: actions/checkout@v2 - - # Setup jfrog cli - - uses: jfrog/setup-jfrog-cli@v1 - with: - version: 1.43.2 - env: - JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - - # Extract build id from input - - name: Extract Build Id - run: | - echo JFROG_CLI_BUILD_NAME="${{ github.event.inputs.buildName }}" >> $GITHUB_ENV - echo JFROG_CLI_BUILD_NUMBER=${{ github.event.inputs.buildNumber }} >> $GITHUB_ENV - - # Download released files - - name: Download Release Files - run: | - jfrog rt download \ - --spec .github/release-files-spec.json \ - --spec-vars "buildname=$JFROG_CLI_BUILD_NAME;buildnumber=$JFROG_CLI_BUILD_NUMBER" - - # Create checksums, signatures and create staging repo on central and upload - - uses: jvalkeal/nexus-sync@v0 - id: nexus - with: - url: ${{ secrets.OSSRH_URL }} - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - create: true - upload: true - generate-checksums: true - pgp-sign: true - pgp-sign-passphrase: ${{ secrets.GPG_PASSPHRASE }} - pgp-sign-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - - # Print staging repo id - - name: Print Staging Repo Id - run: echo ${{ steps.nexus.outputs.staged-repository-id }} diff --git a/.github/workflows/central-sync-release.yml b/.github/workflows/central-sync-release.yml deleted file mode 100644 index abf2cb92f2..0000000000 --- a/.github/workflows/central-sync-release.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Central Sync Release - -on: - workflow_dispatch: - inputs: - stagedRepositoryId: - description: "Staged repository id" - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - # Request release promotion repo - - uses: jvalkeal/nexus-sync@v0 - with: - url: ${{ secrets.OSSRH_URL }} - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - staging-repo-id: ${{ github.event.inputs.stagedRepositoryId }} - release: true diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml new file mode 100644 index 0000000000..7d31cb8e76 --- /dev/null +++ b/.github/workflows/ci-snapshot.yml @@ -0,0 +1,49 @@ +name: CI SNAPSHOT + +on: + workflow_dispatch: + + push: + branches: + - main + - '*.x' + + schedule: + - cron: '0 5 * * *' + +concurrency: + group: group-snapshot-for-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-snapshot: + runs-on: ubuntu-latest + name: CI Build SNAPSHOT for ${{ github.ref_name }} + env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + + steps: + - name: Start RabbitMQ + uses: namoshek/rabbitmq-github-action@v1 + with: + ports: '5672:5672 15672:15672 5552:5552' + plugins: rabbitmq_stream,rabbitmq_management,rabbitmq_delayed_message_exchange,rabbitmq_consistent_hash_exchange + + - uses: actions/checkout@v4 + with: + show-progress: false + + - name: Checkout Common Repo + uses: actions/checkout@v4 + with: + repository: spring-io/spring-github-workflows + path: .github/spring-github-workflows + show-progress: false + + - name: Build and Publish + timeout-minutes: 30 + uses: ./.github/spring-github-workflows/.github/actions/spring-artifactory-gradle-build + with: + gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} + artifactoryUsername: ${{ secrets.ARTIFACTORY_USERNAME }} + artifactoryPassword: ${{ secrets.ARTIFACTORY_PASSWORD }} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000000..2065ee7187 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,19 @@ +name: Deploy Docs + +on: + push: + branches: + - '*.x' + - main + tags: + - '**' + + workflow_dispatch: + +permissions: + actions: write + +jobs: + dispatch-docs-build: + if: github.repository_owner == 'spring-projects' + uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@v5 diff --git a/.github/workflows/manual-deploy-to-central.yml b/.github/workflows/manual-deploy-to-central.yml new file mode 100644 index 0000000000..426be5cebc --- /dev/null +++ b/.github/workflows/manual-deploy-to-central.yml @@ -0,0 +1,26 @@ +name: Manually Deploy Artifactory Build to Maven Central + +on: + workflow_dispatch: + inputs: + buildName: + description: 'Artifactory Build Name' + required: true + type: string + buildNumber: + description: 'Artifactory Build Number' + required: true + type: string + +jobs: + deploy-to-central: + + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-deploy-to-maven-central.yml@main + with: + buildName: ${{ inputs.buildName }} + buildNumber: ${{ inputs.buildNumber }} + secrets: + JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + OSSRH_S01_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} + OSSRH_S01_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} + OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} \ No newline at end of file diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml new file mode 100644 index 0000000000..f513c72567 --- /dev/null +++ b/.github/workflows/merge-dependabot-pr.yml @@ -0,0 +1,18 @@ +name: Merge Dependabot PR + +on: + pull_request: + branches: + - main + - '*.x' + +run-name: Merge Dependabot PR ${{ github.ref_name }} + +jobs: + merge-dependabot-pr: + permissions: write-all + + uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v5 + with: + mergeArguments: --auto --squash + autoMergeSnapshots: true \ No newline at end of file diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml deleted file mode 100644 index 563aa47f78..0000000000 --- a/.github/workflows/pr-build-workflow.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Pull Request build - -on: - pull_request: - branches: [ main ] - -jobs: - build: - - runs-on: ubuntu-latest - - services: - rabbitmq: - image: rabbitmq:management - ports: - - 5672:5672 - - 15762:15762 - - steps: - - uses: actions/checkout@v2 - - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - - name: Run Gradle - uses: burrunan/gradle-cache-action@v1 - with: - arguments: check - - - name: Capture Test Results - if: failure() - uses: actions/upload-artifact@v2 - with: - name: test-results - path: '*/build/reports/tests/**/*.*' - retention-days: 3 diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 0000000000..cefa6bf31a --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,11 @@ +name: Pull Request Build + +on: + pull_request: + branches: + - main + - '*.x' + +jobs: + build-pull-request: + uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..29383edbb7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release + +on: + workflow_dispatch: + +run-name: Release current version for branch ${{ github.ref_name }} + +jobs: + release: + permissions: + actions: write + contents: write + issues: write + + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main + with: + deployMilestoneToCentral: true + secrets: + GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + OSSRH_S01_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} + OSSRH_S01_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} + OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml new file mode 100644 index 0000000000..23074824bb --- /dev/null +++ b/.github/workflows/verify-staged-artifacts.yml @@ -0,0 +1,56 @@ +name: Verify Staged Artifacts + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: 'Release version like 5.0.0-M1, 5.1.0-RC1, 5.2.0 etc.' + required: true + type: string + + +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + +jobs: + verify-staged-with-spring-integration: + runs-on: ubuntu-latest + + steps: + + - name: Start RabbitMQ + uses: namoshek/rabbitmq-github-action@v1 + with: + ports: '5672:5672 15672:15672 5552:5552' + plugins: rabbitmq_stream,rabbitmq_management + + - name: Checkout Spring Integration Repo + uses: actions/checkout@v4 + with: + repository: spring-projects/spring-integration + show-progress: false + + + - name: Set up Gradle + uses: spring-io/spring-gradle-build-action@v2 + + - name: Prepare Spring Integration project against Staging + run: | + printf "allprojects { + repositories { + maven { + url 'https://repo.spring.io/libs-staging-local' + credentials { + username = '$ARTIFACTORY_USERNAME' + password = '$ARTIFACTORY_PASSWORD' + } + } + } + }" > staging-repo-init.gradle + + sed -i "1,/springAmqpVersion.*/s/springAmqpVersion.*/springAmqpVersion='${{ inputs.releaseVersion }}'/" build.gradle + + - name: Verify Spring Integration AMQP module against staged release + run: exit 0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3b3bb7540b..3a4003bf50 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ nohup.out src/ant/.ant-targets-upload-dist.xml target .sts4-cache +.vscode diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 23425ec68a..6a15f897cf 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -19,16 +19,14 @@ https://help.github.com/articles/using-pull-requests/[Using Pull Requests] first == Search GitHub (or JIRA) issues first; create one if necessary Is there already an issue that addresses your concern? -Search the https://github.com/spring-projects/spring-integration/issues[GitHub issue tracker] (and https://jira.spring.io/browse/AMQP[JIRA issue tracker]) to see if you can find something similar. +Search the https://github.com/spring-projects/spring-amqp/issues[GitHub issue tracker] (and https://jira.spring.io/browse/AMQP[JIRA issue tracker]) to see if you can find something similar. If not, please create a new issue in GitHub before submitting a pull request unless the change is truly trivial, e.g. typo fixes, removing compiler warnings, etc. -== Sign the contributor license agreement +== Developer Certificate of Origin -If you have not previously done so, please fill out and submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement (CLA)]. - -Very important, before we can accept any *Spring AMQP contributions*, we will need you to sign the CLA. -Signing the CLA does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. +All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin. +For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring]. == Fork the Repository @@ -51,7 +49,7 @@ _you should see branches on origin as well as upstream, including 'main' and 'ma == A Day in the Life of a Contributor -* _Always_ work on topic branches (Typically use the HitHub (or JIRA) issue ID as the branch name). +* _Always_ work on topic branches (Typically use the GitHub (or JIRA) issue ID as the branch name). - For example, to create and switch to a new branch for issue #123: `git checkout -b GH-123` * You might be working on several different topic branches at any given time, but when at a stopping point for one of those branches, commit (a local operation). * Please follow the "Commit Guidelines" described in https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project[this chapter of Pro Git]. @@ -134,7 +132,7 @@ Please carefully follow the whitespace and formatting conventions already presen 8. Latin-1 (ISO-8859-1) encoding for Java sources; use `native2ascii` to convert if necessary -## Add Apache license header to all new classes +== Add Apache license header to all new classes [source, java] ---- diff --git a/src/dist/license.txt b/LICENSE.txt similarity index 100% rename from src/dist/license.txt rename to LICENSE.txt diff --git a/README.md b/README.md index cc8d252965..f4d82bb7ef 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,17 @@ -Spring AMQP [](https://build.spring.io/browse/AAMQP-MAIN) [![Join the chat at https://gitter.im/spring-projects/spring-amqp](https://badges.gitter.im/spring-projects/spring-amqp.svg)](https://gitter.im/spring-projects/spring-amqp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +Spring AMQP [![Build Status](https://github.com/spring-projects/spring-amqp/actions/workflows/ci-snapshot.yml/badge.svg)](https://github.com/spring-projects/spring-amqp/actions/workflows/ci-snapshot.yml) +[![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-amqp) =========== This project provides support for using Spring and Java with [AMQP 0.9.1](https://www.rabbitmq.com/amqp-0-9-1-reference.html), and in particular [RabbitMQ](https://www.rabbitmq.com/). +# Code of Conduct + +Please see our [Code of conduct](https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md). + +# Reporting Security Vulnerabilities + +Please see our [Security policy](https://github.com/spring-projects/spring-amqp/security/policy). + # Checking out and Building To check out the project from [GitHub](https://github.com/SpringSource/spring-amqp) and build from source using [Gradle](https://gradle.org/), do the following: @@ -15,17 +24,17 @@ If you encounter out of memory errors during the build, increase available heap GRADLE_OPTS='-XX:MaxPermSize=1024m -Xmx1024m' -To build and install jars into your local Maven cache: +To build and publish jars to your local Maven repository: - ./gradlew install + ./gradlew publishToMavenLocal To build api Javadoc (results will be in `build/api`): ./gradlew api -To build reference documentation (results will be in `build/reference`): +To build reference documentation (results will be in `build/site`): - ./gradlew reference + ./gradlew antora To build complete distribution including `-dist`, `-docs`, and `-schema` zip files (results will be in `build/distributions`) @@ -50,15 +59,14 @@ Once complete, you may then import the projects into Eclipse as usual: Browse to the *'spring-amqp'* root directory. All projects should import free of errors. -# Using SpringSource Tool Suite™ (STS) +# Using Spring Tools -Using the STS Gradle Support, you can directly import Gradle projects, without having to generate Eclipse metadata first (since STS 2.7.M1). Please make sure you have the Gradle STS Extension installed - Please see the [installation instructions](https://docs.spring.io/sts/docs/latest/reference/html/gradle/installation.html) for details. +Using the STS Gradle Support, you can directly import Gradle projects, without having to generate Eclipse metadata first. +Please see the [Spring Tools Home Page](https://spring.io/tools). -1. Select *File -> Import -> Gradle Project* +1. Select *File -> Import -> Existing Gradle Project* 2. Browse to the Spring AMQP Root Folder -3. Click on **Build Model** -4. Select the projects you want to import -5. Press **Finish** +3. Click on **Finish** # Using IntelliJ IDEA @@ -66,45 +74,30 @@ To generate IDEA metadata (.iml and .ipr files), do the following: ./gradlew idea -## Distribution Contents - -If you downloaded the full Spring AMQP distribution or if you created the distribution using `./gradlew dist`, you will see the following directory structure: - - ├── README.md - ├── apache-license.txt - ├── docs - │ ├── api - │ └── reference - ├── epl-license.txt - ├── libs - ├── notice.txt - └── schema - └── rabbit - -The binary JARs and the source code are available in the **libs**. The reference manual and javadocs are located in the **docs** directory. - ## Changelog -Lists of issues addressed per release can be found in [JIRA](https://jira.spring.io/browse/AMQP#selectedTab=com.atlassian.jira.plugin.system.project%3Aversions-panel). +Lists of issues addressed per release can be found in [Github](https://github.com/spring-projects/spring-amqp/releases). ## Additional Resources -* [Spring AMQP Homepage](https://www.springsource.org/spring-amqp) +* [Spring AMQP Homepage](https://spring.io/projects/spring-amqp) * [Spring AMQP Source](https://github.com/SpringSource/spring-amqp) * [Spring AMQP Samples](https://github.com/SpringSource/spring-amqp-samples) -* [Spring AMQP Forum](https://forum.spring.io/) * [StackOverflow](https://stackoverflow.com/questions/tagged/spring-amqp) # Contributing to Spring AMQP Here are some ways for you to get involved in the community: -* Get involved with the Spring community on the Spring Community Forums. Please help out on the [forum](https://forum.spring.io/) by responding to questions and joining the debate. -* Create [JIRA](https://jira.spring.io/browse/AMQP) tickets for bugs and new features and comment and vote on the ones that you are interested in. -* Github is for social coding: if you want to write code, we encourage contributions through pull requests from [forks of this repository](https://help.github.com/forking/). If you want to contribute code this way, please reference a JIRA ticket as well covering the specific issue you are addressing. -* Watch for upcoming articles on Spring by [subscribing](https://www.springsource.org/node/feed) to springframework.org +* Get involved with the Spring community on Stack Overflow by responding to questions and joining the debate. + +* Create Github issues for bugs and new features and comment and vote on the ones that you are interested in. +* Github is for social coding: if you want to write code, we encourage contributions through pull requests from [forks of this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo). +If you want to contribute code this way, please reference the specific Github issue you are addressing. -Before we accept a non-trivial patch or pull request we will need you to sign the [contributor's agreement](https://support.springsource.com/spring_committer_signup). Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Active contributors might be asked to join the core team, and given the ability to merge pull requests. +Before we accept a non-trivial patch or pull request we will need you to sign the [contributor's agreement](https://cla.pivotal.io/sign/spring). +Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. +Active contributors might be asked to join the core team, and given the ability to merge pull requests. ## Code Conventions and Housekeeping None of these is essential for a pull request, but they will all help. They can also be added after the original pull request but before a merge. @@ -119,4 +112,4 @@ None of these is essential for a pull request, but they will all help. They can # License -Spring AMQP is released under the terms of the Apache Software License Version 2.0 (see license.txt). +Spring AMQP is released under the terms of the Apache Software License Version 2.0 (see LICENSE.txt). diff --git a/build.gradle b/build.gradle index 5f6f9c7ae0..a94ef9ef1b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ buildscript { - ext.kotlinVersion = '1.4.32' + ext.kotlinVersion = '2.1.20' + ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() - maven { url 'https://plugins.gradle.org/m2' } - maven { url 'https://repo.spring.io/plugins-release' } + gradlePluginPortal() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" @@ -13,60 +13,90 @@ buildscript { plugins { id 'base' - id 'project-report' id 'idea' - id 'org.sonarqube' version '2.8' - id 'org.ajoberstar.grgit' version '4.0.1' - id 'io.spring.nohttp' version '0.0.4.RELEASE' - id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false - id 'com.jfrog.artifactory' version '4.13.0' apply false - id 'org.asciidoctor.jvm.pdf' version '3.1.0' - id 'org.asciidoctor.jvm.gems' version '3.1.0' - id 'org.asciidoctor.jvm.convert' version '3.1.0' + id 'org.ajoberstar.grgit' version '5.3.0' + id 'io.spring.nohttp' version '0.0.11' + id 'io.spring.dependency-management' version '1.1.7' apply false + id 'org.antora' version '1.0.0' + id 'io.spring.antora.generate-antora-yml' version '0.0.1' + id 'io.freefair.aggregate-javadoc' version '8.11' + id 'net.ltgt.errorprone' version '4.1.0' apply false } description = 'Spring AMQP' ext { linkHomepage = 'https://projects.spring.io/spring-amqp' - linkCi = 'https://build.spring.io/browse/AMQP' - linkIssue = 'https://jira.spring.io/browse/AMQP' - linkScmUrl = 'https://github.com/spring-projects/spring-amqp' - linkScmConnection = 'git://github.com/spring-projects/spring-amqp.git' + linkCi = 'https://build.spring.io/browse/AMQP' + linkIssue = 'https://jira.spring.io/browse/AMQP' + linkScmUrl = 'https://github.com/spring-projects/spring-amqp' + linkScmConnection = 'git://github.com/spring-projects/spring-amqp.git' linkScmDevConnection = 'git@github.com:spring-projects/spring-amqp.git' - docResourcesVersion = '0.2.1.RELEASE' modifiedFiles = - files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } - - assertjVersion = '3.20.2' - assertkVersion = '0.24' - awaitilityVersion = '4.1.0' - commonsCompressVersion = '1.20' - commonsHttpClientVersion = '4.5.13' - commonsPoolVersion = '2.10.0' - googleJsr305Version = '3.0.2' - hamcrestVersion = '2.2' - hibernateValidationVersion = '6.2.0.Final' - jacksonBomVersion = '2.12.4' - jaywayJsonPathVersion = '2.4.0' + files() + .from { + files(grgit.status().unstaged.modified) + .filter { f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } + } + modifiedFiles.finalizeValueOnRead() + + assertjVersion = '3.27.3' + assertkVersion = '0.28.1' + awaitilityVersion = '4.3.0' + commonsHttpClientVersion = '5.4.3' + commonsPoolVersion = '2.12.1' + hamcrestVersion = '3.0' + hibernateValidationVersion = '8.0.2.Final' + jacksonBomVersion = '2.18.3' + jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.7.2' - log4jVersion = '2.14.1' - logbackVersion = '1.2.3' - lz4Version = '1.8.0' - micrometerVersion = '1.8.0-M2' - mockitoVersion = '3.11.2' - protonJVersion = '0.33.8' - rabbitmqStreamVersion = '0.1.0' - rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.0' - rabbitmqHttpClientVersion = '3.11.0' - reactorVersion = '2020.0.10' - snappyVersion = '1.1.8.4' - springDataCommonsVersion = '2.6.0-M2' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.9' - springRetryVersion = '1.3.1' - zstdJniVersion = '1.5.0-2' + junitJupiterVersion = '5.12.1' + kotlinCoroutinesVersion = '1.10.1' + log4jVersion = '2.24.3' + logbackVersion = '1.5.18' + micrometerDocsVersion = '1.0.4' + micrometerVersion = '1.15.0-SNAPSHOT' + micrometerTracingVersion = '1.5.0-SNAPSHOT' + mockitoVersion = '5.16.1' + rabbitmqAmqpClientVersion = '0.5.0' + rabbitmqStreamVersion = '0.22.0' + rabbitmqVersion = '5.25.0' + reactorVersion = '2025.0.0-SNAPSHOT' + springDataVersion = '2025.1.0-SNAPSHOT' + springRetryVersion = '2.0.11' + springVersion = '7.0.0-SNAPSHOT' + testcontainersVersion = '1.20.6' + + javaProjects = subprojects - project(':spring-amqp-bom') +} + +antora { + version = '3.2.0-alpha.2' + playbook = file('src/reference/antora/antora-playbook.yml') + options = ['to-dir': project.layout.buildDirectory.dir('site').get().toString(), clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] + dependencies = [ + '@antora/atlas-extension' : '1.0.0-alpha.2', + '@antora/collector-extension' : '1.0.0-beta.3', + '@asciidoctor/tabs' : '1.0.0-beta.6', + '@springio/antora-extensions' : '1.14.2', + '@springio/asciidoctor-extensions': '1.0.0-alpha.14', + ] +} + +tasks.named('generateAntoraYml') { + asciidocAttributes = project.provider( { generateAttributes() } ) + baseAntoraYmlFile = file('src/reference/antora/antora.yml') +} + +tasks.register('createAntoraPartials', Sync) { + from { tasks.filterMetricsDocsContent.outputs } + into layout.buildDirectory.dir('generated-antora-resources/modules/ROOT/partials') +} + +tasks.register('generateAntoraResources') { + dependsOn 'createAntoraPartials' + dependsOn 'generateAntoraYml' } nohttp { @@ -94,14 +124,18 @@ allprojects { mavenBom "org.springframework:spring-framework-bom:$springVersion" mavenBom "io.projectreactor:reactor-bom:$reactorVersion" mavenBom "org.apache.logging.log4j:log4j-bom:$log4jVersion" + mavenBom "org.springframework.data:spring-data-bom:$springDataVersion" + mavenBom "io.micrometer:micrometer-bom:$micrometerVersion" + mavenBom "io.micrometer:micrometer-tracing-bom:$micrometerTracingVersion" + mavenBom "org.testcontainers:testcontainers-bom:$testcontainersVersion" } } repositories { mavenCentral() - maven { url 'https://repo.spring.io/libs-milestone' } + maven { url 'https://repo.spring.io/milestone' } if (version.endsWith('-SNAPSHOT')) { - maven { url 'https://repo.spring.io/libs-snapshot' } + maven { url 'https://repo.spring.io/snapshot' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } // maven { url 'https://repo.spring.io/libs-staging-local' } @@ -111,38 +145,55 @@ allprojects { ext { expandPlaceholders = '**/quick-tour.xml' javadocLinks = [ - 'https://docs.oracle.com/javase/8/docs/api/', - 'https://docs.oracle.com/javaee/7/api/', - 'https://docs.spring.io/spring/docs/current/javadoc-api/' + 'https://docs.oracle.com/en/java/javase/17/docs/api/', + 'https://jakarta.ee/specifications/platform/11/apidocs/', + 'https://docs.spring.io/spring-framework/docs/current/javadoc-api/' ] as String[] } -subprojects { subproject -> +configure(javaProjects) { subproject -> apply plugin: 'java-library' - apply plugin: 'java' - apply from: "${rootProject.projectDir}/publish-maven.gradle" apply plugin: 'eclipse' apply plugin: 'idea' - apply plugin: 'project-report' - apply plugin: 'jacoco' apply plugin: 'checkstyle' apply plugin: 'kotlin' apply plugin: 'kotlin-spring' + apply plugin: 'net.ltgt.errorprone' + + apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" java { + toolchain { + languageVersion = JavaLanguageVersion.of(23) + } withJavadocJar() withSourcesJar() registerFeature('optional') { usingSourceSet(sourceSets.main) } - registerFeature('provided') { - usingSourceSet(sourceSets.main) + } + + tasks.withType(JavaCompile) { + sourceCompatibility = JavaVersion.VERSION_17 + options.encoding = 'UTF-8' + options.errorprone { + disableAllChecks = true + if (!name.toLowerCase().contains('test')) { + option('NullAway:OnlyNullMarked', 'true') + option('NullAway:CustomContractAnnotations', 'org.springframework.lang.Contract') + option('NullAway:JSpecifyMode', 'true') + error('NullAway') + } } } - compileJava { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + compileTestKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + compilerOptions { + allWarningsAsErrors = true + } } eclipse { @@ -151,16 +202,11 @@ subprojects { subproject -> } } - jacoco { - toolVersion = '0.8.5' - } - // dependencies that are common across all java projects dependencies { - compileOnly "com.google.code.findbugs:jsr305:$googleJsr305Version" testImplementation 'org.apache.logging.log4j:log4j-core' testImplementation "org.hamcrest:hamcrest-core:$hamcrestVersion" - testImplementation ("org.mockito:mockito-core:$mockitoVersion") { + testImplementation("org.mockito:mockito-core:$mockitoVersion") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } testImplementation "org.mockito:mockito-junit-jupiter:$mockitoVersion" @@ -176,45 +222,29 @@ subprojects { subproject -> exclude group: 'org.hamcrest' } - // To avoid compiler warnings about @API annotations in JUnit code - testCompileOnly 'org.apiguardian:apiguardian-api:1.0.0' - - testCompileOnly "com.google.code.findbugs:jsr305:$googleJsr305Version" testImplementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - + errorprone 'com.uber.nullaway:nullaway:0.12.6' + errorprone 'com.google.errorprone:error_prone_core:2.36.0' } // enable all compiler warnings; individual projects may customize further - ext.xLintArg = '-Xlint:all,-options,-processing' - [compileJava, compileTestJava]*.options*.compilerArgs = [xLintArg] + ext.xLintArg = '-Xlint:all,-options,-processing,-deprecation' + [compileJava, compileTestJava]*.options*.compilerArgs = [xLintArg, '-parameters'] publishing { publications { mavenJava(MavenPublication) { suppressAllPomMetadataWarnings() from components.java - pom.withXml { - def pomDeps = asNode().dependencies.first() - subproject.configurations.providedImplementation.allDependencies.each { dep -> - pomDeps.remove(pomDeps.'*'.find { it.artifactId.text() == dep.name }) - pomDeps.appendNode('dependency').with { - it.appendNode('groupId', dep.group) - it.appendNode('artifactId', dep.name) - it.appendNode('version', dep.version) - it.appendNode('scope', 'provided') - } - } - } } } } - task updateCopyrights { - onlyIf { !System.getenv('GITHUB_ACTION') && !System.getenv('bamboo_buildKey') } + tasks.register('updateCopyrights') { + onlyIf { !isCI } inputs.files(modifiedFiles.filter { f -> f.path.contains(subproject.name) }) - outputs.dir('build/classes') doLast { def now = Calendar.instance.get(Calendar.YEAR) as String @@ -227,7 +257,7 @@ subprojects { subproject -> def beginningYear = matcher[0][1] if (now != beginningYear && now != matcher[0][2]) { def years = "$beginningYear-$now" - def sourceCode = file.text + def sourceCode = file.getText('UTF-8') sourceCode = sourceCode.replaceFirst(/20\d\d(-20\d\d)?/, years) file.text = sourceCode println "Copyright updated for file: $file" @@ -240,9 +270,21 @@ subprojects { subproject -> } } - compileKotlin.dependsOn updateCopyrights + compileKotlin.dependsOn updateCopyrights + + tasks.withType(JavaForkOptions) { + jvmArgs '--add-opens', 'java.base/java.util.zip=ALL-UNNAMED' + } + + tasks.withType(Javadoc) { + options.addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint + options.addBooleanOption('Werror', true) // fail build on Javadoc warnings + } test { + maxHeapSize = '2g' + jvmArgs '-XX:+HeapDumpOnOutOfMemoryError' + testLogging { events "skipped", "failed" showStandardStreams = project.hasProperty("showStandardStreams") ?: false @@ -250,46 +292,33 @@ subprojects { subproject -> showStackTraces = true exceptionFormat = 'full' } - - jacoco { - destinationFile = file("$buildDir/jacoco.exec") - } - - if (System.properties['sonar.host.url']) { - finalizedBy jacocoTestReport - } } - jacocoTestReport { - reports { - xml.enabled true - csv.enabled false - html.enabled false - xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") - } + tasks.register('testAll', Test) { + dependsOn check } - task testAll(type: Test, dependsOn: check) - gradle.taskGraph.whenReady { graph -> if (graph.hasTask(testAll)) { test.enabled = false } } - tasks.withType(Test).all { + tasks.withType(Test).configureEach { // suppress all console output during testing unless running `gradle -i` logging.captureStandardOutput(LogLevel.INFO) if (name ==~ /(testAll)/) { systemProperty 'RUN_LONG_INTEGRATION_TESTS', 'true' } + environment "SPRING_AMQP_DESERIALIZATION_TRUST_ALL", "true" + useJUnitPlatform() } checkstyle { - configDirectory.set(rootProject.file("src/checkstyle")) - toolVersion = '8.24' + configDirectory.set(rootProject.file('src/checkstyle')) + toolVersion = '10.21.4' } jar { @@ -299,46 +328,48 @@ subprojects { subproject -> 'Created-By': "JDK ${System.properties['java.version']} (${System.properties['java.specification.vendor']})", 'Implementation-Title': subproject.name, 'Implementation-Vendor-Id': subproject.group, - 'Implementation-Vendor': 'Pivotal Software, Inc.', + 'Implementation-Vendor': 'Broadcom Inc.', 'Implementation-URL': linkHomepage, 'Automatic-Module-Name': subproject.name.replace('-', '.') // for Jigsaw ) } from("${rootProject.projectDir}/src/dist") { - include 'license.txt' include 'notice.txt' into 'META-INF' expand(copyright: new Date().format('yyyy'), version: project.version) } + from("${rootProject.projectDir}") { + include 'LICENSE.txt' + into 'META-INF' + } } check.dependsOn javadoc - } project('spring-amqp') { description = 'Spring AMQP Core' dependencies { - api 'org.springframework:spring-core' + api("org.springframework.retry:spring-retry:$springRetryVersion") { + exclude group: 'org.springframework' + } optionalApi 'org.springframework:spring-messaging' optionalApi 'org.springframework:spring-oxm' optionalApi 'org.springframework:spring-context' - api ("org.springframework.retry:spring-retry:$springRetryVersion") { - exclude group: 'org.springframework' - } - optionalApi 'com.fasterxml.jackson.core:jackson-core' optionalApi 'com.fasterxml.jackson.core:jackson-databind' optionalApi 'com.fasterxml.jackson.core:jackson-annotations' optionalApi 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' - + optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' + optionalApi 'com.fasterxml.jackson.module:jackson-module-parameter-names' + optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-joda' + optionalApi 'com.fasterxml.jackson.module:jackson-module-kotlin' // Spring Data projection message binding support - optionalApi ("org.springframework.data:spring-data-commons:$springDataCommonsVersion") { - exclude group: 'org.springframework' - } + optionalApi 'org.springframework.data:spring-data-commons' optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" testImplementation "org.assertj:assertj-core:$assertjVersion" @@ -346,230 +377,233 @@ project('spring-amqp') { } +project('spring-amqp-bom') { + description = 'Spring for RabbitMQ (Bill of Materials)' + + apply plugin: 'java-platform' + apply from: "${rootDir}/gradle/publish-maven.gradle" + + dependencies { + constraints { + javaProjects.sort { "$it.name" }.each { + api it + } + } + } + + publishing { + publications { + mavenJava(MavenPublication) { + from components.javaPlatform + } + } + } +} + project('spring-rabbit') { description = 'Spring RabbitMQ Support' dependencies { - api project(':spring-amqp') api "com.rabbitmq:amqp-client:$rabbitmqVersion" - optionalApi "com.rabbitmq:http-client:$rabbitmqHttpClientVersion" - optionalApi 'org.springframework:spring-aop' api 'org.springframework:spring-context' api 'org.springframework:spring-messaging' api 'org.springframework:spring-tx' + api 'io.micrometer:micrometer-observation' + + optionalApi 'org.springframework:spring-aop' + optionalApi 'org.springframework:spring-webflux' + optionalApi "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" optionalApi 'io.projectreactor:reactor-core' + optionalApi 'io.projectreactor.netty:reactor-netty-http' optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' - optionalApi "io.micrometer:micrometer-core:$micrometerVersion" + optionalApi 'io.micrometer:micrometer-core' + optionalApi 'io.micrometer:micrometer-tracing' // Spring Data projection message binding support - optionalApi ("org.springframework.data:spring-data-commons:$springDataCommonsVersion") { - exclude group: 'org.springframework' - } + optionalApi 'org.springframework.data:spring-data-commons' optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" optionalApi "org.apache.commons:commons-pool2:$commonsPoolVersion" + optionalApi "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinCoroutinesVersion" testApi project(':spring-rabbit-junit') + testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion") testImplementation "org.hibernate.validator:hibernate-validator:$hibernateValidationVersion" - testRuntimeOnly 'org.springframework:spring-web' - testRuntimeOnly "org.apache.httpcomponents:httpclient:$commonsHttpClientVersion" + testImplementation 'io.micrometer:micrometer-observation-test' + testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' + testImplementation 'io.micrometer:micrometer-tracing-test' + testImplementation 'io.micrometer:micrometer-tracing-integration-test' + testImplementation 'org.testcontainers:rabbitmq' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" + testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' - testRuntimeOnly ("junit:junit:$junit4Version") { + testRuntimeOnly("junit:junit:$junit4Version") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } - } - - compileTestKotlin { - kotlinOptions { - jvmTarget = '1.8' - } - } - } project('spring-rabbit-stream') { description = 'Spring RabbitMQ Stream Support' dependencies { - api project(':spring-rabbit') api "com.rabbitmq:stream-client:$rabbitmqStreamVersion" - optionalApi "com.rabbitmq:http-client:$rabbitmqHttpClientVersion" + optionalApi 'io.micrometer:micrometer-core' testApi project(':spring-rabbit-junit') + testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' - testRuntimeOnly "org.apache.httpcomponents:httpclient:$commonsHttpClientVersion" - testRuntimeOnly "org.apache.qpid:proton-j:$protonJVersion" - testRuntimeOnly "org.apache.commons:commons-compress:$commonsCompressVersion" - testRuntimeOnly "org.xerial.snappy:snappy-java:$snappyVersion" - testRuntimeOnly "org.lz4:lz4-java:$lz4Version" - testRuntimeOnly "com.github.luben:zstd-jni:$zstdJniVersion" - testImplementation "org.testcontainers:rabbitmq:1.15.3" - testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" + + testImplementation 'org.testcontainers:rabbitmq' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl' + testImplementation 'org.springframework:spring-webflux' + testImplementation 'io.micrometer:micrometer-observation-test' + testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' + testImplementation 'io.micrometer:micrometer-tracing-test' + testImplementation 'io.micrometer:micrometer-tracing-integration-test' } +} + +project('spring-rabbitmq-client') { + description = 'Spring RabbitMQ Client for AMQP 1.0' + + dependencies { + api project(':spring-rabbit') + api "com.rabbitmq.client:amqp-client:$rabbitmqAmqpClientVersion" + + testApi project(':spring-rabbit-junit') + testApi 'io.projectreactor:reactor-core' + + testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' + testImplementation 'org.testcontainers:rabbitmq' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl' + } } project('spring-rabbit-junit') { description = 'Spring Rabbit JUnit Support' dependencies { // no spring-amqp dependencies allowed - api 'org.springframework:spring-core' api 'org.springframework:spring-test' - optionalApi ("junit:junit:$junit4Version") { - exclude group: 'org.hamcrest', module: 'hamcrest-core' - } api "com.rabbitmq:amqp-client:$rabbitmqVersion" - api ("com.rabbitmq:http-client:$rabbitmqHttpClientVersion") { - exclude group: 'org.springframework', module: 'spring-web' - } api 'org.springframework:spring-web' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" + + optionalApi("junit:junit:$junit4Version") { + exclude group: 'org.hamcrest', module: 'hamcrest-core' + } + optionalApi 'org.testcontainers:rabbitmq' + optionalApi 'org.testcontainers:junit-jupiter' optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' compileOnly 'org.apiguardian:apiguardian-api:1.0.0' - } - } project('spring-rabbit-test') { description = 'Spring Rabbit Test Support' dependencies { - api project(':spring-rabbit') api project(':spring-rabbit-junit') api "org.hamcrest:hamcrest-library:$hamcrestVersion" api "org.hamcrest:hamcrest-core:$hamcrestVersion" api "org.mockito:mockito-core:$mockitoVersion" + testImplementation project(':spring-rabbit').sourceSets.test.output } - } configurations { - docs + micrometerDocs } dependencies { - docs "io.spring.docresources:spring-doc-resources:${docResourcesVersion}@zip" + micrometerDocs "io.micrometer:micrometer-docs-generator:$micrometerDocsVersion" } -task prepareAsciidocBuild(type: Sync) { - dependsOn configurations.docs - from { - configurations.docs.collect { zipTree(it) } - } - from 'src/reference/asciidoc/' - into "$buildDir/asciidoc" +def observationInputDir = file('build/docs/microsources').absolutePath +def generatedDocsDir = file('build/docs/generated').absolutePath + +tasks.register('copyObservation', Copy) { + from file('spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer').absolutePath + from file('spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer').absolutePath + include '*.java' + exclude 'package-info.java' + into observationInputDir } -asciidoctorPdf { - dependsOn prepareAsciidocBuild - baseDirFollowsSourceFile() +tasks.register('generateObservabilityDocs', JavaExec) { + dependsOn copyObservation + mainClass = 'io.micrometer.docs.DocsGeneratorCommand' + inputs.dir(observationInputDir) + outputs.dir(generatedDocsDir) + classpath configurations.micrometerDocs + args observationInputDir, /.+/, generatedDocsDir +} - asciidoctorj { - sourceDir "$buildDir/asciidoc" - inputs.dir(sourceDir) - sources { - include 'index.adoc' - } - options doctype: 'book' - attributes 'icons': 'font', - 'sectanchors': '', - 'sectnums': '', - 'toc': '', - 'source-highlighter' : 'coderay', - revnumber: project.version, - 'project-version': project.version - } -} - -asciidoctor { - dependsOn asciidoctorPdf - baseDirFollowsSourceFile() - sourceDir "$buildDir/asciidoc" - resources { - from(sourceDir) { - include 'images/*', 'css/**', 'js/**' - } - } - options doctype: 'book', eruby: 'erubis' - - attributes 'docinfo': 'shared', - stylesdir: "css/", - stylesheet: 'spring.css', - 'linkcss': true, - 'icons': 'font', - 'sectanchors': '', - 'source-highlighter': 'highlight.js', - 'highlightjsdir': 'js/highlight', - 'highlightjs-theme': 'github', - 'idprefix': '', - 'idseparator': '-', - 'spring-version': project.version, - 'allow-uri-read': '', - 'toc': 'left', - 'toclevbels': '4', - revnumber: project.version, - 'project-version': project.version -} - -task reference(dependsOn: asciidoctor) { - group = 'Documentation' - description = 'Generate the reference documentation' +tasks.register('filterMetricsDocsContent', Copy) { + dependsOn generateObservabilityDocs + from generatedDocsDir + include '_*.adoc' + into generatedDocsDir + rename { filename -> filename.replace '_', '' } + filter { line -> line.replaceAll('org.springframework.*.micrometer.', '').replaceAll('^Fully qualified n', 'N') } } -sonarqube { - properties { - property 'sonar.links.homepage', linkHomepage - property 'sonar.links.ci', linkCi - property 'sonar.links.issue', linkIssue - property 'sonar.links.scm', linkScmUrl - property 'sonar.links.scm_dev', linkScmDevConnection +dependencies { + javaProjects.each { + javadoc it } } -task api(type: Javadoc) { - group = 'Documentation' - description = 'Generates aggregated Javadoc API documentation.' +javadoc { title = "${rootProject.description} ${version} API" - options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED - options.author = true - options.header = rootProject.description - options.overview = 'src/api/overview.html' - options.stylesheetFile = file('src/api/stylesheet.css') - options.links(rootProject.ext.javadocLinks) + options { + encoding = 'UTF-8' + memberLevel = JavadocMemberLevel.PROTECTED + author = true + header = project.description + use = true + overview = 'src/api/overview.html' + splitIndex = true + links(project.ext.javadocLinks) + addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint + } + + destinationDir = file('build/api') + classpath = files().from { files(javaProjects.collect { it.sourceSets.main.compileClasspath }) } +} - source subprojects.collect { project -> - project.sourceSets.main.allJava - } - destinationDir = new File(buildDir, 'api') - classpath = files(subprojects.collect { project -> - project.sourceSets.main.compileClasspath - }) +tasks.register('api') { + group = 'Documentation' + description = 'Generates aggregated Javadoc API documentation.' + dependsOn javadoc } -task schemaZip(type: Zip) { +tasks.register('schemaZip', Zip) { group = 'Distribution' archiveClassifier = 'schema' description = "Builds -${archiveClassifier} archive containing all " + - "XSDs for deployment at static.springframework.org/schema." + "XSDs for deployment at static.springframework.org/schema." - subprojects.each { subproject -> - def Set files = new HashSet() - def Properties schemas = new Properties(); + javaProjects.each { subproject -> + Set files = new HashSet() + Properties schemas = new Properties(); def shortName = subproject.name.replaceFirst("${rootProject.name}-", '') if (subproject.name.endsWith('-rabbit')) { @@ -586,7 +620,7 @@ task schemaZip(type: Zip) { } assert xsdFile != null if (!files.contains(xsdFile.path)) { - into ("${shortName}") { + into("${shortName}") { from xsdFile.path rename { String fileName -> String[] versionNumbers = project.version.split(/\./, 3) @@ -599,73 +633,75 @@ task schemaZip(type: Zip) { } } -task docsZip(type: Zip, dependsOn: [reference]) { - group = 'Distribution' - archiveClassifier = 'docs' - description = "Builds -${archiveClassifier} archive containing api and reference " + +tasks.register('docsZip', Zip) { + group = 'Distribution' + archiveClassifier = 'docs' + description = "Builds -${archiveClassifier} archive containing api and reference " + "for deployment at static.springframework.org/spring-integration/docs." - from('src/dist') { - include 'changelog.txt' - } - - from (api) { - into 'api' - } - - from ('build/docs/asciidoc') { - into 'reference/html' - } + from('src/dist') { + include 'changelog.txt' + } - from ('build/docs/asciidocPdf') { - include 'index.pdf' - rename 'index.pdf', 'spring-amqp-reference.pdf' - into 'reference/pdf' - } + from(javadoc) { + into 'api' + } } -task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { +tasks.register('distZip', Zip) { + dependsOn docsZip + dependsOn schemaZip group = 'Distribution' archiveClassifier = 'dist' description = "Builds -${archiveClassifier} archive, containing all jars and docs, " + - "suitable for community download page." + "suitable for community download page." ext.baseDir = "${project.name}-${project.version}"; from('src/dist') { include 'README.md' - include 'apache-license.txt' - include 'epl-license.txt' include 'notice.txt' into "${baseDir}" } - from(zipTree(docsZip.archivePath)) { + from("$project.rootDir") { + include 'LICENSE.txt' + into "${baseDir}" + } + + from(zipTree(docsZip.archiveFile)) { into "${baseDir}/docs" } - from(zipTree(schemaZip.archivePath)) { + from(zipTree(schemaZip.archiveFile)) { into "${baseDir}/schema" } - subprojects.each { subproject -> - into ("${baseDir}/libs") { + javaProjects.each { subproject -> + into("${baseDir}/libs") { from subproject.jar from subproject.sourcesJar from subproject.javadocJar } } + + from(project(':spring-amqp-bom').generatePomFileForMavenJavaPublication) { + into "${baseDir}/libs" + rename 'pom-default.xml', "spring-amqp-bom-${project.version}.xml" + } + } // Create an optional "with dependencies" distribution. // Not published by default; only for use when building from source. -task depsZip(type: Zip, dependsOn: distZip) { zipTask -> +tasks.register('depsZip', Zip) { + dependsOn distZip group = 'Distribution' archiveClassifier = 'dist-with-deps' description = "Builds -${archiveClassifier} archive, containing everything " + - "in the -${distZip.archiveClassifier} archive plus all dependencies." + "in the -${distZip.archiveClassifier} archive plus all dependencies." - from zipTree(distZip.archivePath) + from zipTree(distZip.archiveFile) gradle.taskGraph.whenReady { taskGraph -> if (taskGraph.hasTask(":${zipTask.name}")) { @@ -689,18 +725,13 @@ task depsZip(type: Zip, dependsOn: distZip) { zipTask -> tasks.build.dependsOn assemble -artifacts { - archives distZip - archives docsZip - archives schemaZip -} - -task dist(dependsOn: assemble) { +tasks.register('dist') { + dependsOn assemble group = 'Distribution' description = 'Builds -dist, -docs and -schema distribution archives.' } -apply from: "${rootProject.projectDir}/publish-maven.gradle" +apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" publishing { publications { @@ -711,3 +742,24 @@ publishing { } } } + +def generateAttributes() { + def springDocs = "https://docs.spring.io" + def micrometerDocsPrefix = "https://docs.micrometer.io" + + return [ + 'project-version': project.version, + 'spring-integration-docs': "$springDocs/spring-integration/reference".toString(), + 'spring-framework-docs': "$springDocs/spring-framework/reference/${generateVersionWithoutPatch(springVersion)}".toString(), + 'spring-retry-java-docs': "$springDocs/spring-retry/docs/$springRetryVersion/apidocs".toString(), + 'javadoc-location-org-springframework-transaction': "$springDocs/spring-framework/docs/$springVersion/javadoc-api".toString(), + 'javadoc-location-org-springframework-amqp': "$springDocs/spring-amqp/docs/$project.version/api".toString(), + 'micrometer-docs': "$micrometerDocsPrefix/micrometer/reference/${generateVersionWithoutPatch(micrometerVersion)}".toString(), + 'micrometer-tracing-docs': "$micrometerDocsPrefix/tracing/reference/${generateVersionWithoutPatch(micrometerTracingVersion)}".toString() + ] +} + +static String generateVersionWithoutPatch(String version) { + + return version.split('\\.')[0,1].join('.') + (version.endsWith('-SNAPSHOT') ? '-SNAPSHOT' : '') +} diff --git a/eclipse-code-formatter.xml b/eclipse-code-formatter.xml deleted file mode 100644 index e2a217491d..0000000000 --- a/eclipse-code-formatter.xml +++ /dev/null @@ -1,313 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gradle.properties b/gradle.properties index 2eae223f7f..0e863135dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,6 @@ -version=2.4.0-M2 -org.gradlee.caching=true +version=4.0.0-SNAPSHOT +org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true +org.gradle.caching=true org.gradle.parallel=true kotlin.stdlib.default.dependency=false diff --git a/publish-maven.gradle b/gradle/publish-maven.gradle similarity index 71% rename from publish-maven.gradle rename to gradle/publish-maven.gradle index 336bbc582e..e3a05676e0 100644 --- a/publish-maven.gradle +++ b/gradle/publish-maven.gradle @@ -1,5 +1,4 @@ apply plugin: 'maven-publish' -apply plugin: 'com.jfrog.artifactory' publishing { publications { @@ -27,31 +26,41 @@ publishing { developerConnection = linkScmDevConnection } developers { + developer { + id = 'artembilan' + name = 'Artem Bilan' + email = 'artem.bilan@broadcom.com' + roles = ['project lead'] + } developer { id = 'garyrussell' name = 'Gary Russell' - email = 'grussell@vmware.com' - roles = ['project lead'] + email = 'github@gprussell.net' + roles = ['project lead emeritus'] } developer { - id = 'artembilan' - name = 'Artem Bilan' - email = 'abilan@vmware.com' + id = 'sobychacko' + name = 'Soby Chacko' + email = 'soby.chacko@broadcom.com' + roles = ['contributor'] } developer { - id = 'davesyer' + id = 'dsyer' name = 'Dave Syer' - email = 'dsyer@vmware.com' + email = 'david.syer@broadcom.com' + roles = ['project founder'] } developer { id = 'markfisher' name = 'Mark Fisher' - email = 'markfisher@vmware.com' + email = 'mark.ryan.fisher@gmail.com' + roles = ['project founder'] } developer { id = 'markpollack' name = 'Mark Pollack' - email = 'mpollack@vmware.com' + email = 'mark.pollack@broadcom.com' + roles = ['project founder'] } } issueManagement { @@ -70,7 +79,3 @@ publishing { } } } - -artifactoryPublish { - publications(publishing.publications.mavenJava) -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023..a4b76b9530 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 9027973dc5..d71047787f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip +distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=8de6efc274ab52332a9c820366dd5cf5fc9d35ec7078fd70c8ec6913431ee610 diff --git a/gradlew b/gradlew index 4f906e0c81..f3b75f3b0d 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,69 +15,103 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# 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/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f938..9b42019c79 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @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 +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +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 @@ -56,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 @@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/settings.gradle b/settings.gradle index 5d950cd7c3..f67d410c94 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,18 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +plugins { + id 'io.spring.develocity.conventions' version '0.0.22' +} + rootProject.name = 'spring-amqp-dist' -include 'spring-amqp' -include 'spring-rabbit' -include 'spring-rabbit-stream' -include 'spring-rabbit-junit' -include 'spring-rabbit-test' +rootDir.eachDir { dir -> + if (dir.name.startsWith('spring-')) { + include ":${dir.name}" + } +} \ No newline at end of file diff --git a/spring-amqp-bom/spring-amqp-bom.txt b/spring-amqp-bom/spring-amqp-bom.txt new file mode 100644 index 0000000000..9bf4012144 --- /dev/null +++ b/spring-amqp-bom/spring-amqp-bom.txt @@ -0,0 +1 @@ +This meta-project is used to generate a bill-of-materials POM that contains the other projects in a dependencyManagement section. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/AmqpException.java b/spring-amqp/src/main/java/org/springframework/amqp/AmqpException.java index 7e6aea5128..9e329c8810 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/AmqpException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/AmqpException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,13 @@ package org.springframework.amqp; +import org.jspecify.annotations.Nullable; + /** * Base RuntimeException for errors that occur when executing AMQP operations. * * @author Mark Fisher + * @author Artem Bilan */ @SuppressWarnings("serial") public class AmqpException extends RuntimeException { @@ -28,11 +31,11 @@ public AmqpException(String message) { super(message); } - public AmqpException(Throwable cause) { + public AmqpException(@Nullable Throwable cause) { super(cause); } - public AmqpException(String message, Throwable cause) { + public AmqpException(@Nullable String message, @Nullable Throwable cause) { super(message, cause); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/AmqpRejectAndDontRequeueException.java b/spring-amqp/src/main/java/org/springframework/amqp/AmqpRejectAndDontRequeueException.java index 168e39d6cd..56863b5a28 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/AmqpRejectAndDontRequeueException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/AmqpRejectAndDontRequeueException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,16 @@ package org.springframework.amqp; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Exception for listener implementations used to indicate the - * basic.reject will be sent with requeue=false in order to enable - * features such as DLQ. + * {@code basic.reject} will be sent with {@code requeue=false} + * in order to enable features such as DLQ. + * * @author Gary Russell + * @author Artem Bilan + * * @since 1.0.1 * */ diff --git a/spring-amqp/src/main/java/org/springframework/amqp/UncategorizedAmqpException.java b/spring-amqp/src/main/java/org/springframework/amqp/UncategorizedAmqpException.java index 3612bd06bd..06e33d08d2 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/UncategorizedAmqpException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/UncategorizedAmqpException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.amqp; +import org.jspecify.annotations.Nullable; + /** * A "catch-all" exception type within the AmqpException hierarchy * when no more specific cause is known. @@ -25,7 +27,7 @@ @SuppressWarnings("serial") public class UncategorizedAmqpException extends AmqpException { - public UncategorizedAmqpException(Throwable cause) { + public UncategorizedAmqpException(@Nullable Throwable cause) { super(cause); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java index fe09c2b6d6..add559ba60 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author 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,29 +19,34 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Base class for builders supporting arguments. * * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan + * * @since 1.6 * */ public abstract class AbstractBuilder { - private Map arguments; + private @Nullable Map arguments; /** * Return the arguments map, after creating one if necessary. * @return the arguments. */ - protected Map getOrCreateArguments() { + protected Map getOrCreateArguments() { if (this.arguments == null) { - this.arguments = new LinkedHashMap(); + this.arguments = new LinkedHashMap<>(); } return this.arguments; } - protected Map getArguments() { + protected @Nullable Map getArguments() { return this.arguments; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java index 3f662d1447..fd640038b1 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,26 +22,33 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * Base class for {@link Declarable} classes. * * @author Gary Russell + * @author Christian Tzolov + * @author Ngoc Nhan * @since 1.2 * */ public abstract class AbstractDeclarable implements Declarable { - private boolean shouldDeclare = true; + private final Lock lock = new ReentrantLock(); - private Collection declaringAdmins = new ArrayList(); + protected final Map arguments; + + private boolean shouldDeclare = true; private boolean ignoreDeclarationExceptions; - private final Map arguments; + private Collection declaringAdmins = new ArrayList<>(); public AbstractDeclarable() { this(null); @@ -52,12 +59,12 @@ public AbstractDeclarable() { * @param arguments the arguments. * @since 2.2.2 */ - public AbstractDeclarable(@Nullable Map arguments) { + public AbstractDeclarable(@Nullable Map arguments) { if (arguments != null) { this.arguments = new HashMap<>(arguments); } else { - this.arguments = new HashMap(); + this.arguments = new HashMap<>(); } } @@ -67,7 +74,7 @@ public boolean shouldDeclare() { } /** - * Whether or not this object should be automatically declared + * Whether this object should be automatically declared * by any {@code AmqpAdmin}. Default is {@code true}. * @param shouldDeclare true or false. */ @@ -95,27 +102,40 @@ public void setIgnoreDeclarationExceptions(boolean ignoreDeclarationExceptions) } @Override - public void setAdminsThatShouldDeclare(Object... adminArgs) { - Collection admins = new ArrayList(); + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public void setAdminsThatShouldDeclare(@Nullable Object @Nullable ... adminArgs) { + Collection admins = new ArrayList<>(); if (adminArgs != null) { if (adminArgs.length > 1) { Assert.noNullElements(adminArgs, "'admins' cannot contain null elements"); } if (adminArgs.length > 0 && !(adminArgs.length == 1 && adminArgs[0] == null)) { - admins.addAll(Arrays.asList(adminArgs)); + admins = Arrays.asList(adminArgs); } } this.declaringAdmins = admins; } @Override - public synchronized void addArgument(String argName, Object argValue) { - this.arguments.put(argName, argValue); + public void addArgument(String argName, Object argValue) { + this.lock.lock(); + try { + this.arguments.put(argName, argValue); + } + finally { + this.lock.unlock(); + } } @Override - public synchronized Object removeArgument(String name) { - return this.arguments.remove(name); + public Object removeArgument(String name) { + this.lock.lock(); + try { + return this.arguments.remove(name); + } + finally { + this.lock.unlock(); + } } public Map getArguments() { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractExchange.java index 9accd5d8a1..57dc6c41e6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; /** * Common properties that describe all exchange types. @@ -71,7 +72,9 @@ public AbstractExchange(String name, boolean durable, boolean autoDelete) { * longer in use * @param arguments the arguments used to declare the exchange */ - public AbstractExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public AbstractExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(arguments); this.name = name; this.durable = durable; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java index c1d10642fb..36a940e5f8 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Address.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,14 +16,15 @@ package org.springframework.amqp.core; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.util.StringUtils; /** - * Represents an address for publication of an AMQP message. The AMQP 0-8 and 0-9 - * specifications have an unstructured string that is used as a "reply to" address. + * Represents an address for publication of an AMQP message. The AMQP 0.9 + * specification has an unstructured string that is used as a "reply to" address. * There are however conventions in use and this class makes it easier to * follow these conventions, which can be easily summarised as: * @@ -32,13 +33,17 @@ * * * Here we also the exchange name to default to empty - * (so just a routing key will work if you know the queue name). + * (so just a routing key will work as a queue name). + *

+ * For AMQP 1.0, only routing key is treated as target destination. + * * * @author Mark Pollack * @author Mark Fisher * @author Dave Syer * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan */ public class Address { @@ -56,11 +61,11 @@ public class Address { /** * Create an Address instance from a structured String with the form - * *

 	 * (exchange)/(routingKey)
 	 * 
* . + * If exchange is parsed to empty string, then routing key is treated as a queue name. * @param address a structured string. */ public Address(String address) { @@ -111,28 +116,16 @@ public String getRoutingKey() { @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - Address address = (Address) o; - - return !(this.exchangeName != null - ? !this.exchangeName.equals(address.exchangeName) - : address.exchangeName != null) - && !(this.routingKey != null - ? !this.routingKey.equals(address.routingKey) - : address.routingKey != null); + return o instanceof Address address + && Objects.equals(this.exchangeName, address.exchangeName) + && Objects.equals(this.routingKey, address.routingKey); } @Override public int hashCode() { - int result = this.exchangeName != null ? this.exchangeName.hashCode() : 0; + int result = this.exchangeName.hashCode(); int prime = 31; // NOSONAR magic # - result = prime * result + (this.routingKey != null ? this.routingKey.hashCode() : 0); + result = prime * result + this.routingKey.hashCode(); return result; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAcknowledgment.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAcknowledgment.java new file mode 100644 index 0000000000..8b9b94d047 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAcknowledgment.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.core; + +/** + * An abstraction over acknowledgments. + * + * @author Artem Bilan + * + * @since 4.0 + */ +@FunctionalInterface +public interface AmqpAcknowledgment { + + /** + * Acknowledge the message. + * @param status the status. + */ + void acknowledge(Status status); + + default void acknowledge() { + acknowledge(Status.ACCEPT); + } + + enum Status { + + /** + * Mark the message as accepted. + */ + ACCEPT, + + /** + * Mark the message as rejected. + */ + REJECT, + + /** + * Reject the message and requeue so that it will be redelivered. + */ + REQUEUE + + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java index 62d8566d34..97117fbfa2 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package org.springframework.amqp.core; +import java.util.Collections; import java.util.Properties; +import java.util.Set; -import org.springframework.lang.Nullable; - +import org.jspecify.annotations.Nullable; /** * Specifies a basic set of portable AMQP administrative operations for AMQP > 0.9. @@ -27,6 +28,7 @@ * @author Mark Pollack * @author Dave Syer * @author Gary Russell + * @author Artem Bilan */ public interface AmqpAdmin { @@ -126,6 +128,15 @@ public interface AmqpAdmin { @Nullable QueueInformation getQueueInfo(String queueName); + /** + * Return the manually declared AMQP objects. + * @return the manually declared AMQP objects. + * @since 2.4.15 + */ + default Set getManualDeclarableSet() { + return Collections.emptySet(); + } + /** * Initialize the admin. * @since 2.1 diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java index 4cb710336e..4cc5bea3c9 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2025 the original author or 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.amqp.core; +import java.io.Serial; + import org.springframework.amqp.AmqpException; /** @@ -28,16 +30,10 @@ */ public class AmqpMessageReturnedException extends AmqpException { + @Serial private static final long serialVersionUID = 1866579721126554167L; - private final ReturnedMessage returned; - - @Deprecated - public AmqpMessageReturnedException(String message, Message returnedMessage, int replyCode, String replyText, - String exchange, String routingKey) { - - this(message, new ReturnedMessage(returnedMessage, replyCode, replyText, exchange, routingKey)); - } + private final transient ReturnedMessage returned; public AmqpMessageReturnedException(String message, ReturnedMessage returned) { super(message); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java index 37ad90fbc6..1cd0dec7ac 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ package org.springframework.amqp.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; /** * Specifies a basic set of AMQP operations. - * + *

* Provides synchronous send and receive methods. The {@link #convertAndSend(Object)} and * {@link #receiveAndConvert()} methods allow let you send and receive POJO objects. * Implementations are expected to delegate to an instance of @@ -34,6 +35,7 @@ * @author Artem Bilan * @author Ernest Sadykov * @author Gary Russell + * @author Artem Bilan */ public interface AmqpTemplate { @@ -250,8 +252,7 @@ void convertAndSend(String exchange, String routingKey, Object message, MessageP * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException; + @Nullable T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException; /** * Receive a message if there is one from a specific queue and convert it to a Java @@ -265,8 +266,7 @@ void convertAndSend(String exchange, String routingKey, Object message, MessageP * @throws AmqpException if there is a problem * @since 2.0 */ - @Nullable - T receiveAndConvert(String queueName, ParameterizedTypeReference type) throws AmqpException; + @Nullable T receiveAndConvert(String queueName, ParameterizedTypeReference type) throws AmqpException; /** * Receive a message if there is one from a default queue and convert it to a Java @@ -283,8 +283,7 @@ void convertAndSend(String exchange, String routingKey, Object message, MessageP * @throws AmqpException if there is a problem * @since 2.0 */ - @Nullable - T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) throws AmqpException; + @Nullable T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) throws AmqpException; /** * Receive a message if there is one from a specific queue and convert it to a Java @@ -302,8 +301,7 @@ void convertAndSend(String exchange, String routingKey, Object message, MessageP * @throws AmqpException if there is a problem * @since 2.0 */ - @Nullable - T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) + @Nullable T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) throws AmqpException; // receive and send methods for provided callback @@ -419,9 +417,9 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * and attempt to receive a response. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Message sendAndReceive(Message message) throws AmqpException; @@ -431,10 +429,10 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * and attempt to receive a response. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param routingKey the routing key - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param routingKey the routing key. + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Message sendAndReceive(String routingKey, Message message) throws AmqpException; @@ -445,11 +443,11 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * reply-to header to an exclusive queue and wait up for some time limited by a * timeout. * - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Message sendAndReceive(String exchange, String routingKey, Message message) throws AmqpException; @@ -462,9 +460,9 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(Object message) throws AmqpException; @@ -475,10 +473,10 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param routingKey the routing key - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param routingKey the routing key. + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(String routingKey, Object message) throws AmqpException; @@ -489,11 +487,11 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(String exchange, String routingKey, Object message) throws AmqpException; @@ -504,10 +502,10 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(Object message, MessagePostProcessor messagePostProcessor) throws AmqpException; @@ -518,11 +516,11 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param routingKey the routing key - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param routingKey the routing key. + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(String routingKey, Object message, MessagePostProcessor messagePostProcessor) @@ -534,12 +532,12 @@ Object convertSendAndReceive(String routingKey, Object message, MessagePostProce * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(String exchange, String routingKey, Object message, @@ -555,12 +553,11 @@ Object convertSendAndReceive(String exchange, String routingKey, Object message, * @param message a message to send. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one. + * @return the response; or null if the reply times out. * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(Object message, ParameterizedTypeReference responseType) + @Nullable T convertSendAndReceiveAsType(Object message, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -569,16 +566,15 @@ T convertSendAndReceiveAsType(Object message, ParameterizedTypeReference * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param routingKey the routing key - * @param message a message to send + * @param routingKey the routing key. + * @param message a message to send. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String routingKey, Object message, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -587,17 +583,16 @@ T convertSendAndReceiveAsType(String routingKey, Object message, * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -606,16 +601,15 @@ T convertSendAndReceiveAsType(String exchange, String routingKey, Object mes * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePostProcessor, + @Nullable T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -624,17 +618,16 @@ T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePo * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param routingKey the routing key - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent + * @param routingKey the routing key. + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String routingKey, Object message, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException; @@ -644,18 +637,17 @@ T convertSendAndReceiveAsType(String routingKey, Object message, * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AnonymousQueue.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AnonymousQueue.java index e4ea9c52bf..fd0390c6d6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AnonymousQueue.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AnonymousQueue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,14 +18,17 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Represents an anonymous, non-durable, exclusive, auto-delete queue. The name has the - * form 'spring.gen-<base64UUID>' by default, but it can be modified by providing a + * form {@code spring.gen-} by default, but it can be modified by providing a * {@link NamingStrategy}. Two naming strategies {@link Base64UrlNamingStrategy} and - * {@link UUIDNamingStrategy} are provided but you can implement your own. Names should be + * {@link UUIDNamingStrategy} are provided, but you can implement your own. Names should be * unique. * @author Dave Syer * @author Gary Russell + * @author Artem Bilan * */ public class AnonymousQueue extends Queue { @@ -34,15 +37,15 @@ public class AnonymousQueue extends Queue { * Construct a queue with a Base64-based name. */ public AnonymousQueue() { - this((Map) null); + this((Map) null); } /** * Construct a queue with a Base64-based name with the supplied arguments. * @param arguments the arguments. */ - public AnonymousQueue(Map arguments) { - this(org.springframework.amqp.core.Base64UrlNamingStrategy.DEFAULT, arguments); + public AnonymousQueue(@Nullable Map arguments) { + this(Base64UrlNamingStrategy.DEFAULT, arguments); } /** @@ -62,9 +65,12 @@ public AnonymousQueue(org.springframework.amqp.core.NamingStrategy namingStrateg * @param arguments the arguments. * @since 2.1 */ - public AnonymousQueue(org.springframework.amqp.core.NamingStrategy namingStrategy, Map arguments) { + @SuppressWarnings("this-escape") + public AnonymousQueue(org.springframework.amqp.core.NamingStrategy namingStrategy, + @Nullable Map arguments) { + super(namingStrategy.generateName(), false, true, true, arguments); - if (!getArguments().containsKey(X_QUEUE_LEADER_LOCATOR)) { + if (!this.arguments.containsKey(X_QUEUE_LEADER_LOCATOR)) { setLeaderLocator("client-local"); } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java index cd84b043f1..7e8c00fe5f 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,35 +16,125 @@ package org.springframework.amqp.core; +import java.util.concurrent.CompletableFuture; + +import org.jspecify.annotations.Nullable; + import org.springframework.core.ParameterizedTypeReference; -import org.springframework.util.concurrent.ListenableFuture; /** * Classes implementing this interface can perform asynchronous send and - * receive operations. + * receive operations using {@link CompletableFuture}s. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 * */ public interface AsyncAmqpTemplate { + default CompletableFuture send(Message message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture send(String queue, Message message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture send(String exchange, @Nullable String routingKey, Message message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(Object message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(String queue, Object message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(String queue, Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + throw new UnsupportedOperationException(); + } + + default CompletableFuture receive() { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receive(String queueName) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndConvert() { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndConvert(String queueName) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndConvert(@Nullable ParameterizedTypeReference type) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndConvert(String queueName, @Nullable ParameterizedTypeReference type) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndReply(ReceiveAndReplyCallback callback) { + throw new UnsupportedOperationException(); + } + + /** + * Perform a server-side RPC functionality. + * The request message must have a {@code replyTo} property. + * The request {@code messageId} property is used for correlation. + * The callback might not produce a reply with the meaning nothing to answer. + * @param the request body type. + * @param the response body type + * @param queueName the queue to consume request. + * @param callback an application callback to handle request and produce reply. + * @return the completion status: true if no errors and reply has been produced. + */ + default CompletableFuture receiveAndReply(String queueName, ReceiveAndReplyCallback callback) { + throw new UnsupportedOperationException(); + } + /** * Send a message to the default exchange with the default routing key. If the message * contains a correlationId property, it must be unique. * @param message the message. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture sendAndReceive(Message message); + CompletableFuture sendAndReceive(Message message); /** * Send a message to the default exchange with the supplied routing key. If the message * contains a correlationId property, it must be unique. * @param routingKey the routing key. * @param message the message. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture sendAndReceive(String routingKey, Message message); + CompletableFuture sendAndReceive(String routingKey, Message message); /** * Send a message to the supplied exchange and routing key. If the message @@ -52,18 +142,18 @@ public interface AsyncAmqpTemplate { * @param exchange the exchange. * @param routingKey the routing key. * @param message the message. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture sendAndReceive(String exchange, String routingKey, Message message); + CompletableFuture sendAndReceive(String exchange, String routingKey, Message message); /** * Convert the object to a message and send it to the default exchange with the * default routing key. * @param object the object to convert. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(Object object); + CompletableFuture convertSendAndReceive(Object object); /** * Convert the object to a message and send it to the default exchange with the @@ -71,9 +161,9 @@ public interface AsyncAmqpTemplate { * @param routingKey the routing key. * @param object the object to convert. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(String routingKey, Object object); + CompletableFuture convertSendAndReceive(String routingKey, Object object); /** * Convert the object to a message and send it to the provided exchange and @@ -82,9 +172,9 @@ public interface AsyncAmqpTemplate { * @param routingKey the routing key. * @param object the object to convert. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(String exchange, String routingKey, Object object); + CompletableFuture convertSendAndReceive(String exchange, String routingKey, Object object); /** * Convert the object to a message and send it to the default exchange with the @@ -93,9 +183,9 @@ public interface AsyncAmqpTemplate { * @param object the object to convert. * @param messagePostProcessor the post processor. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(Object object, MessagePostProcessor messagePostProcessor); + CompletableFuture convertSendAndReceive(Object object, MessagePostProcessor messagePostProcessor); /** * Convert the object to a message and send it to the default exchange with the @@ -105,9 +195,9 @@ public interface AsyncAmqpTemplate { * @param object the object to convert. * @param messagePostProcessor the post processor. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(String routingKey, Object object, + CompletableFuture convertSendAndReceive(String routingKey, Object object, MessagePostProcessor messagePostProcessor); /** @@ -119,10 +209,10 @@ ListenableFuture convertSendAndReceive(String routingKey, Object object, * @param object the object to convert. * @param messagePostProcessor the post processor. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor); + CompletableFuture convertSendAndReceive(String exchange, String routingKey, Object object, + @Nullable MessagePostProcessor messagePostProcessor); /** * Convert the object to a message and send it to the default exchange with the @@ -130,9 +220,9 @@ ListenableFuture convertSendAndReceive(String exchange, String routingKey * @param object the object to convert. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(Object object, ParameterizedTypeReference responseType); + CompletableFuture convertSendAndReceiveAsType(Object object, ParameterizedTypeReference responseType); /** * Convert the object to a message and send it to the default exchange with the @@ -141,9 +231,9 @@ ListenableFuture convertSendAndReceive(String exchange, String routingKey * @param object the object to convert. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(String routingKey, Object object, + CompletableFuture convertSendAndReceiveAsType(String routingKey, Object object, ParameterizedTypeReference responseType); /** @@ -154,9 +244,9 @@ ListenableFuture convertSendAndReceiveAsType(String routingKey, Object ob * @param object the object to convert. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, + CompletableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, ParameterizedTypeReference responseType); /** @@ -167,10 +257,11 @@ ListenableFuture convertSendAndReceiveAsType(String exchange, String rout * @param messagePostProcessor the post processor. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(Object object, MessagePostProcessor messagePostProcessor, - ParameterizedTypeReference responseType); + CompletableFuture convertSendAndReceiveAsType(Object object, + @Nullable MessagePostProcessor messagePostProcessor, + @Nullable ParameterizedTypeReference responseType); /** * Convert the object to a message and send it to the default exchange with the @@ -181,10 +272,10 @@ ListenableFuture convertSendAndReceiveAsType(Object object, MessagePostPr * @param messagePostProcessor the post processor. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType); + CompletableFuture convertSendAndReceiveAsType(String routingKey, Object object, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType); /** * Convert the object to a message and send it to the provided exchange and @@ -196,9 +287,9 @@ ListenableFuture convertSendAndReceiveAsType(String routingKey, Object ob * @param messagePostProcessor the post processor. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType); + CompletableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Base64UrlNamingStrategy.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Base64UrlNamingStrategy.java index 1572600bfc..b6c6fd5a59 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Base64UrlNamingStrategy.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Base64UrlNamingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2023 the original author 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,10 @@ package org.springframework.amqp.core; import java.nio.ByteBuffer; +import java.util.Base64; import java.util.UUID; import org.springframework.util.Assert; -import org.springframework.util.Base64Utils; /** * Generates names with the form {@code } where 'prefix' is @@ -66,7 +66,7 @@ public String generateName() { bb.putLong(uuid.getMostSignificantBits()) .putLong(uuid.getLeastSignificantBits()); // Convert to base64 and remove trailing = - return this.prefix + Base64Utils.encodeToUrlSafeString(bb.array()) + return this.prefix + Base64.getUrlEncoder().encodeToString(bb.array()) .replaceAll("=", ""); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java new file mode 100644 index 0000000000..99ee99d17a --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java @@ -0,0 +1,195 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.core; + +import java.util.Arrays; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * An {@link AbstractBuilder} extension for generics support. + * + * @param the target class implementation type. + * + * @author Gary Russell + * @author Artem Bilan + * + * @since 3.2 + * + */ +public abstract class BaseExchangeBuilder> extends AbstractBuilder { + + protected final String name; + + protected final String type; + + protected boolean durable = true; + + protected boolean autoDelete; + + protected boolean internal; + + private boolean delayed; + + private boolean ignoreDeclarationExceptions; + + private boolean declare = true; + + private @Nullable Object @Nullable [] declaringAdmins; + + /** + * Construct an instance of the appropriate type. + * @param name the exchange name + * @param type the type name + * @since 1.6.7 + * @see ExchangeTypes + */ + public BaseExchangeBuilder(String name, String type) { + this.name = name; + this.type = type; + } + + + /** + * Set the auto delete flag. + * @return the builder. + */ + public B autoDelete() { + this.autoDelete = true; + return _this(); + } + + /** + * Set the durable flag. + * @param isDurable the durable flag (default true). + * @return the builder. + */ + public B durable(boolean isDurable) { + this.durable = isDurable; + return _this(); + } + + /** + * Add an argument. + * @param key the argument key. + * @param value the argument value. + * @return the builder. + */ + public B withArgument(String key, Object value) { + getOrCreateArguments().put(key, value); + return _this(); + } + + /** + * Add the arguments. + * @param arguments the arguments map. + * @return the builder. + */ + public B withArguments(Map arguments) { + this.getOrCreateArguments().putAll(arguments); + return _this(); + } + + public B alternate(String exchange) { + return withArgument("alternate-exchange", exchange); + } + + /** + * Set the internal flag. + * @return the builder. + */ + public B internal() { + this.internal = true; + return _this(); + } + + /** + * Set the delayed flag. + * @return the builder. + */ + public B delayed() { + this.delayed = true; + return _this(); + } + + /** + * Switch on ignore exceptions such as mismatched properties when declaring. + * @return the builder. + * @since 2.0 + */ + public B ignoreDeclarationExceptions() { + this.ignoreDeclarationExceptions = true; + return _this(); + } + + /** + * Switch to disable declaration of the exchange by any admin. + * @return the builder. + * @since 2.1 + */ + public B suppressDeclaration() { + this.declare = false; + return _this(); + } + + /** + * Admin instances, or admin bean names that should declare this exchange. + * @param admins the admins. + * @return the builder. + * @since 2.1 + */ + public B admins(Object... admins) { + Assert.notNull(admins, "'admins' cannot be null"); + Assert.noNullElements(admins, "'admins' can't have null elements"); + this.declaringAdmins = Arrays.copyOf(admins, admins.length); + return _this(); + } + + @SuppressWarnings("unchecked") + public T build() { + AbstractExchange exchange = switch (this.type) { + case ExchangeTypes.DIRECT -> new DirectExchange(this.name, this.durable, this.autoDelete, getArguments()); + case ExchangeTypes.TOPIC -> new TopicExchange(this.name, this.durable, this.autoDelete, getArguments()); + case ExchangeTypes.FANOUT -> new FanoutExchange(this.name, this.durable, this.autoDelete, getArguments()); + case ExchangeTypes.HEADERS -> new HeadersExchange(this.name, this.durable, this.autoDelete, getArguments()); + default -> new CustomExchange(this.name, this.type, this.durable, this.autoDelete, getArguments()); + }; + + return (T) configureExchange(exchange); + } + + protected T configureExchange(T exchange) { + exchange.setInternal(this.internal); + exchange.setDelayed(this.delayed); + exchange.setIgnoreDeclarationExceptions(this.ignoreDeclarationExceptions); + exchange.setShouldDeclare(this.declare); + if (!ObjectUtils.isEmpty(this.declaringAdmins)) { + exchange.setAdminsThatShouldDeclare(this.declaringAdmins); + } + return exchange; + } + + @SuppressWarnings("unchecked") + protected final B _this() { + return (B) this; + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java index abaf75e432..3008323f94 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,9 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; /** * Simple container collecting information to describe a binding. Takes String destination and exchange names as @@ -29,6 +31,8 @@ * @author Mark Fisher * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan * * @see AmqpAdmin */ @@ -50,37 +54,57 @@ public enum DestinationType { EXCHANGE; } - private final String destination; + private final @Nullable String destination; - private final String exchange; + private final @Nullable String exchange; - private final String routingKey; + private final @Nullable String routingKey; private final DestinationType destinationType; - public Binding(String destination, DestinationType destinationType, String exchange, String routingKey, - @Nullable Map arguments) { + private final @Nullable Queue lazyQueue; + + public Binding(String destination, DestinationType destinationType, @Nullable String exchange, String routingKey, + @Nullable Map arguments) { + + this(null, destination, destinationType, exchange, routingKey, arguments); + } + + public Binding(@Nullable Queue lazyQueue, @Nullable String destination, DestinationType destinationType, + @Nullable String exchange, @Nullable String routingKey, @Nullable Map arguments) { super(arguments); + Assert.isTrue(lazyQueue == null || destinationType == DestinationType.QUEUE, + "'lazyQueue' must be null for destination type " + destinationType); + Assert.isTrue(lazyQueue != null || destination != null, "`destination` cannot be null"); + this.lazyQueue = lazyQueue; this.destination = destination; this.destinationType = destinationType; this.exchange = exchange; this.routingKey = routingKey; } - public String getDestination() { - return this.destination; + public @Nullable String getDestination() { + if (this.lazyQueue != null) { + return this.lazyQueue.getActualName(); + } + else { + return this.destination; + } } public DestinationType getDestinationType() { return this.destinationType; } - public String getExchange() { + public @Nullable String getExchange() { return this.exchange; } - public String getRoutingKey() { + public @Nullable String getRoutingKey() { + if (this.routingKey == null && this.lazyQueue != null) { + return this.lazyQueue.getActualName(); + } return this.routingKey; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java index b1105d2550..cacfc3920c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package org.springframework.amqp.core; -import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Binding.DestinationType; import org.springframework.util.Assert; @@ -30,6 +31,8 @@ * @author Mark Fisher * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan */ public final class BindingBuilder { @@ -37,15 +40,20 @@ private BindingBuilder() { } public static DestinationConfigurer bind(Queue queue) { - return new DestinationConfigurer(queue.getName(), DestinationType.QUEUE); + if (queue.getName().isEmpty()) { + return new DestinationConfigurer(queue, DestinationType.QUEUE); + } + else { + return new DestinationConfigurer(queue.getName(), DestinationType.QUEUE); + } } public static DestinationConfigurer bind(Exchange exchange) { return new DestinationConfigurer(exchange.getName(), DestinationType.EXCHANGE); } - private static Map createMapForKeys(String... keys) { - Map map = new HashMap(); + private static Map createMapForKeys(String... keys) { + Map map = new HashMap<>(); for (String key : keys) { map.put(key, null); } @@ -57,17 +65,26 @@ private static Map createMapForKeys(String... keys) { */ public static final class DestinationConfigurer { - protected final String name; // NOSONAR + protected final @Nullable String name; // NOSONAR protected final DestinationType type; // NOSONAR + protected final @Nullable Queue queue; // NOSONAR + DestinationConfigurer(String name, DestinationType type) { + this.queue = null; this.name = name; this.type = type; } + DestinationConfigurer(Queue queue, DestinationType type) { + this.queue = queue; + this.name = null; + this.type = type; + } + public Binding to(FanoutExchange exchange) { - return new Binding(this.name, this.type, exchange.getName(), "", new HashMap()); + return new Binding(this.queue, this.name, this.type, exchange.getName(), "", new HashMap<>()); } public HeadersExchangeMapConfigurer to(HeadersExchange exchange) { @@ -85,6 +102,7 @@ public TopicExchangeRoutingKeyConfigurer to(TopicExchange exchange) { public GenericExchangeRoutingKeyConfigurer to(Exchange exchange) { return new GenericExchangeRoutingKeyConfigurer(this, exchange); } + } /** @@ -134,18 +152,21 @@ public final class HeadersExchangeSingleValueBindingCreator { } public Binding exists() { - return new Binding(HeadersExchangeMapConfigurer.this.destination.name, + return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, + HeadersExchangeMapConfigurer.this.destination.name, HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", createMapForKeys(this.key)); } public Binding matches(Object value) { - Map map = new HashMap(); + Map map = new HashMap<>(); map.put(this.key, value); - return new Binding(HeadersExchangeMapConfigurer.this.destination.name, + return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, + HeadersExchangeMapConfigurer.this.destination.name, HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", map); } + } /** @@ -153,7 +174,7 @@ public Binding matches(Object value) { */ public final class HeadersExchangeKeysBindingCreator { - private final Map headerMap; + private final Map headerMap; HeadersExchangeKeysBindingCreator(String[] headerKeys, boolean matchAll) { Assert.notEmpty(headerKeys, "header key list must not be empty"); @@ -162,10 +183,12 @@ public final class HeadersExchangeKeysBindingCreator { } public Binding exist() { - return new Binding(HeadersExchangeMapConfigurer.this.destination.name, + return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, + HeadersExchangeMapConfigurer.this.destination.name, HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", this.headerMap); } + } /** @@ -173,20 +196,23 @@ public Binding exist() { */ public final class HeadersExchangeMapBindingCreator { - private final Map headerMap; + private final Map headerMap; HeadersExchangeMapBindingCreator(Map headerMap, boolean matchAll) { Assert.notEmpty(headerMap, "header map must not be empty"); - this.headerMap = new HashMap(headerMap); + this.headerMap = new HashMap<>(headerMap); this.headerMap.put("x-match", (matchAll ? "all" : "any")); } public Binding match() { - return new Binding(HeadersExchangeMapConfigurer.this.destination.name, + return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, + HeadersExchangeMapConfigurer.this.destination.name, HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", this.headerMap); } + } + } private abstract static class AbstractRoutingKeyConfigurer { @@ -199,6 +225,7 @@ private abstract static class AbstractRoutingKeyConfigurer { this.destination = destination; this.exchange = exchange; } + } /** @@ -211,14 +238,14 @@ public static final class TopicExchangeRoutingKeyConfigurer extends AbstractRout } public Binding with(String routingKey) { - return new Binding(destination.name, destination.type, exchange, routingKey, - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, null); } public Binding with(Enum routingKeyEnum) { - return new Binding(destination.name, destination.type, exchange, routingKeyEnum.toString(), - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, + routingKeyEnum.toString(), null); } + } /** @@ -254,14 +281,16 @@ public GenericArgumentsConfigurer(GenericExchangeRoutingKeyConfigurer configurer this.routingKey = routingKey; } - public Binding and(Map map) { - return new Binding(this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, + public Binding and(Map map) { + return new Binding(this.configurer.destination.queue, + this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, this.routingKey, map); } public Binding noargs() { - return new Binding(this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, - this.routingKey, Collections.emptyMap()); + return new Binding(this.configurer.destination.queue, + this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, + this.routingKey, null); } } @@ -276,19 +305,18 @@ public static final class DirectExchangeRoutingKeyConfigurer extends AbstractRou } public Binding with(String routingKey) { - return new Binding(destination.name, destination.type, exchange, routingKey, - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, null); } public Binding with(Enum routingKeyEnum) { - return new Binding(destination.name, destination.type, exchange, routingKeyEnum.toString(), - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, + routingKeyEnum.toString(), null); } public Binding withQueueName() { - return new Binding(destination.name, destination.type, exchange, destination.name, - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, destination.name, null); } + } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java new file mode 100644 index 0000000000..24402bd058 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java @@ -0,0 +1,102 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.core; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * An {@link AbstractExchange} extension for Consistent Hash exchange type. + * + * @author Artem Bilan + * + * @since 3.2 + * + * @see AmqpAdmin + */ +public class ConsistentHashExchange extends AbstractExchange { + + /** + * Construct a new durable, non-auto-delete Exchange with the provided name. + * @param name the name of the exchange. + */ + public ConsistentHashExchange(String name) { + super(name); + } + + /** + * Construct a new Exchange, given a name, durability flag, auto-delete flag. + * @param name the name of the exchange. + * @param durable true if we are declaring a durable exchange (the exchange will + * survive a server restart) + * @param autoDelete true if the server should delete the exchange when it is no + * longer in use + */ + public ConsistentHashExchange(String name, boolean durable, boolean autoDelete) { + super(name, durable, autoDelete); + } + + /** + * Construct a new Exchange, given a name, durability flag, and auto-delete flag, and + * arguments. + * @param name the name of the exchange. + * @param durable true if we are declaring a durable exchange (the exchange will + * survive a server restart) + * @param autoDelete true if the server should delete the exchange when it is no + * longer in use + * @param arguments the arguments used to declare the exchange + */ + public ConsistentHashExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + + super(name, durable, autoDelete, arguments); + Assert.isTrue(arguments == null || + (!(arguments.containsKey("hash-header") && arguments.containsKey("hash-property"))), + "The 'hash-header' and 'hash-property' are mutually exclusive."); + } + + /** + * Specify a header name from the message to hash. + * @param headerName the header name for hashing. + */ + public void setHashHeader(String headerName) { + Map arguments = getArguments(); + Assert.isTrue(!arguments.containsKey("hash-property"), + "The 'hash-header' and 'hash-property' are mutually exclusive."); + arguments.put("hash-header", headerName); + } + + /** + * Specify a property name from the message to hash. + * @param propertyName the property name for hashing. + */ + public void setHashProperty(String propertyName) { + Map arguments = getArguments(); + Assert.isTrue(!arguments.containsKey("hash-header"), + "The 'hash-header' and 'hash-property' are mutually exclusive."); + arguments.put("hash-property", propertyName); + } + + @Override + public String getType() { + return ExchangeTypes.CONSISTENT_HASH; + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/CustomExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/CustomExchange.java index 3d9e8f9cc1..3980470c64 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/CustomExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/CustomExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,16 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Simple container collecting information to describe a custom exchange. Custom exchange types are allowed by the AMQP * specification, and their names should start with "x-" (but this is not enforced here). Used in conjunction with * administrative operations. + * * @author Dave Syer + * @author Artem Bilan + * * @see AmqpAdmin */ public class CustomExchange extends AbstractExchange { @@ -39,7 +44,9 @@ public CustomExchange(String name, String type, boolean durable, boolean autoDel this.type = type; } - public CustomExchange(String name, String type, boolean durable, boolean autoDelete, Map arguments) { + public CustomExchange(String name, String type, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); this.type = type; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java index da6d4788d9..b2035cf7ba 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.util.Collection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Classes implementing this interface can be auto-declared @@ -26,14 +26,15 @@ * Registration can be limited to specific {@code AmqpAdmin}s. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.2 * */ public interface Declarable { /** - * Whether or not this object should be automatically declared - * by any {@code AmqpAdmin}. + * Whether this object should be automatically declared by any {@code AmqpAdmin}. * @return true if the object should be declared. */ boolean shouldDeclare(); @@ -47,7 +48,7 @@ public interface Declarable { /** * Should ignore exceptions (such as mismatched args) when declaring. - * @return true if should ignore. + * @return true if it should ignore. * @since 1.6 */ boolean isIgnoreDeclarationExceptions(); @@ -61,7 +62,7 @@ public interface Declarable { * the behavior such that all admins will declare the object. * @param adminArgs The admins. */ - void setAdminsThatShouldDeclare(Object... adminArgs); + void setAdminsThatShouldDeclare(@Nullable Object... adminArgs); /** * Add an argument to the declarable. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java index a6726d85d2..e2624dbdc6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2024 the original author 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 @@ * broker using a single bean declaration for the collection. * * @author Gary Russell + * @author Björn Michael * @since 2.1 */ public class Declarables { @@ -42,7 +43,7 @@ public Declarables(Declarable... declarables) { } } - public Declarables(Collection declarables) { + public Declarables(Collection declarables) { Assert.notNull(declarables, "declarables cannot be null"); this.declarables.addAll(declarables); } @@ -58,11 +59,10 @@ public Collection getDeclarables() { * @return the filtered list. * @since 2.2 */ - @SuppressWarnings("unchecked") - public List getDeclarablesByType(Class type) { + public List getDeclarablesByType(Class type) { return this.declarables.stream() .filter(type::isInstance) - .map(dec -> (T) dec) + .map(type::cast) .collect(Collectors.toList()); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/DirectExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/DirectExchange.java index e7565ce819..dbbfb10bb4 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/DirectExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/DirectExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,16 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Simple container collecting information to describe a direct exchange. * Used in conjunction with administrative operations. + * * @author Mark Pollack * @author Dave Syer + * @author Artem Bilan + * * @see AmqpAdmin */ public class DirectExchange extends AbstractExchange { @@ -41,7 +46,9 @@ public DirectExchange(String name, boolean durable, boolean autoDelete) { super(name, durable, autoDelete); } - public DirectExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public DirectExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java index d99b9c98f9..20d977b133 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeBuilder.java index af3405fade..273a9b84ca 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 the original author or 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,6 @@ package org.springframework.amqp.core; -import java.util.Arrays; -import java.util.Map; - -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; - /** * Builder providing a fluent API for building {@link Exchange}s. * @@ -29,27 +23,8 @@ * @author Artem Bilan * * @since 1.6 - * */ -public final class ExchangeBuilder extends AbstractBuilder { - - private final String name; - - private final String type; - - private boolean durable = true; - - private boolean autoDelete; - - private boolean internal; - - private boolean delayed; - - private boolean ignoreDeclarationExceptions; - - private boolean declare = true; - - private Object[] declaringAdmins; +public class ExchangeBuilder extends BaseExchangeBuilder { /** * Construct an instance of the appropriate type. @@ -59,8 +34,7 @@ public final class ExchangeBuilder extends AbstractBuilder { * @see ExchangeTypes */ public ExchangeBuilder(String name, String type) { - this.name = name; - this.type = type; + super(name, type); } /** @@ -100,126 +74,49 @@ public static ExchangeBuilder headersExchange(String name) { } /** - * Set the auto delete flag. - * @return the builder. - */ - public ExchangeBuilder autoDelete() { - this.autoDelete = true; - return this; - } - - /** - * Set the durable flag. - * @param isDurable the durable flag (default true). - * @return the builder. - */ - public ExchangeBuilder durable(boolean isDurable) { - this.durable = isDurable; - return this; - } - - /** - * Add an argument. - * @param key the argument key. - * @param value the argument value. - * @return the builder. - */ - public ExchangeBuilder withArgument(String key, Object value) { - getOrCreateArguments().put(key, value); - return this; - } - - /** - * Add the arguments. - * @param arguments the arguments map. - * @return the builder. - */ - public ExchangeBuilder withArguments(Map arguments) { - this.getOrCreateArguments().putAll(arguments); - return this; - } - - public ExchangeBuilder alternate(String exchange) { - return withArgument("alternate-exchange", exchange); - } - - /** - * Set the internal flag. - * @return the builder. - */ - public ExchangeBuilder internal() { - this.internal = true; - return this; - } - - /** - * Set the delayed flag. - * @return the builder. - */ - public ExchangeBuilder delayed() { - this.delayed = true; - return this; - } - - /** - * Switch on ignore exceptions such as mismatched properties when declaring. - * @return the builder. - * @since 2.0 - */ - public ExchangeBuilder ignoreDeclarationExceptions() { - this.ignoreDeclarationExceptions = true; - return this; - } - - /** - * Switch to disable declaration of the exchange by any admin. + * Return an {@code x-consistent-hash} exchange builder. + * @param name the name. * @return the builder. - * @since 2.1 + * @since 3.2 */ - public ExchangeBuilder suppressDeclaration() { - this.declare = false; - return this; + public static ConsistentHashExchangeBuilder consistentHashExchange(String name) { + return new ConsistentHashExchangeBuilder(name); } /** - * Admin instances, or admin bean names that should declare this exchange. - * @param admins the admins. - * @return the builder. - * @since 2.1 + * An {@link ExchangeBuilder} extension for the {@link ConsistentHashExchange}. + * + * @since 3.2 */ - public ExchangeBuilder admins(Object... admins) { - Assert.notNull(admins, "'admins' cannot be null"); - Assert.noNullElements(admins, "'admins' can't have null elements"); - this.declaringAdmins = Arrays.copyOf(admins, admins.length); - return this; - } - - @SuppressWarnings("unchecked") - public T build() { - AbstractExchange exchange; - if (ExchangeTypes.DIRECT.equals(this.type)) { - exchange = new DirectExchange(this.name, this.durable, this.autoDelete, getArguments()); - } - else if (ExchangeTypes.TOPIC.equals(this.type)) { - exchange = new TopicExchange(this.name, this.durable, this.autoDelete, getArguments()); + public static final class ConsistentHashExchangeBuilder extends BaseExchangeBuilder { + + /** + * Construct an instance of the builder for {@link ConsistentHashExchange}. + * + * @param name the exchange name + * @see ExchangeTypes + */ + public ConsistentHashExchangeBuilder(String name) { + super(name, ExchangeTypes.CONSISTENT_HASH); } - else if (ExchangeTypes.FANOUT.equals(this.type)) { - exchange = new FanoutExchange(this.name, this.durable, this.autoDelete, getArguments()); - } - else if (ExchangeTypes.HEADERS.equals(this.type)) { - exchange = new HeadersExchange(this.name, this.durable, this.autoDelete, getArguments()); + + public ConsistentHashExchangeBuilder hashHeader(String headerName) { + withArgument("hash-header", headerName); + return this; } - else { - exchange = new CustomExchange(this.name, this.type, this.durable, this.autoDelete, getArguments()); + + public ConsistentHashExchangeBuilder hashProperty(String propertyName) { + withArgument("hash-property", propertyName); + return this; } - exchange.setInternal(this.internal); - exchange.setDelayed(this.delayed); - exchange.setIgnoreDeclarationExceptions(this.ignoreDeclarationExceptions); - exchange.setShouldDeclare(this.declare); - if (!ObjectUtils.isEmpty(this.declaringAdmins)) { - exchange.setAdminsThatShouldDeclare(this.declaringAdmins); + + @Override + @SuppressWarnings("unchecked") + public ConsistentHashExchange build() { + return configureExchange( + new ConsistentHashExchange(this.name, this.durable, this.autoDelete, getArguments())); } - return (T) exchange; + } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeTypes.java b/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeTypes.java index 291d940fe9..472d9ece8b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeTypes.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeTypes.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,8 +21,9 @@ * * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan */ -public abstract class ExchangeTypes { +public final class ExchangeTypes { /** * Direct exchange. @@ -44,9 +45,20 @@ public abstract class ExchangeTypes { */ public static final String HEADERS = "headers"; + /** + * Consistent Hash exchange. + * @since 3.2 + */ + public static final String CONSISTENT_HASH = "x-consistent-hash"; + /** * System exchange. + * @deprecated with no replacement (for removal): there is no such an exchange type in AMQP. */ + @Deprecated(since = "3.2", forRemoval = true) public static final String SYSTEM = "system"; + private ExchangeTypes() { + } + } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/FanoutExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/FanoutExchange.java index 44ea3e0b89..8a2e137d1d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/FanoutExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/FanoutExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,16 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Simple container collecting information to describe a fanout exchange. * Used in conjunction with administrative operations. + * * @author Mark Pollack * @author Dave Syer + * @author Artem Bilan + * * @see AmqpAdmin */ public class FanoutExchange extends AbstractExchange { @@ -35,7 +40,9 @@ public FanoutExchange(String name, boolean durable, boolean autoDelete) { super(name, durable, autoDelete); } - public FanoutExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public FanoutExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/HeadersExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/HeadersExchange.java index 892d9ab06f..4866175956 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/HeadersExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/HeadersExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,14 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Headers exchange. * * @author Mark Fisher * @author Dave Syer + * @author Artem Bilan */ public class HeadersExchange extends AbstractExchange { @@ -34,7 +37,9 @@ public HeadersExchange(String name, boolean durable, boolean autoDelete) { super(name, durable, autoDelete); } - public HeadersExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public HeadersExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java index 15e9c6c727..4039013615 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.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,16 +16,11 @@ package org.springframework.amqp.core; -import java.io.ByteArrayInputStream; import java.io.Serializable; import java.nio.charset.Charset; import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; -import org.springframework.amqp.utils.SerializationUtils; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * The 0-8 and 0-9-1 AMQP specifications do not define an Message class or interface. Instead, when performing an @@ -41,6 +36,7 @@ * @author Gary Russell * @author Alex Panchenko * @author Artem Bilan + * @author Ngoc Nhan */ public class Message implements Serializable { @@ -48,11 +44,12 @@ public class Message implements Serializable { private static final String DEFAULT_ENCODING = Charset.defaultCharset().name(); - private static final Set ALLOWED_LIST_PATTERNS = - new LinkedHashSet<>(Arrays.asList("java.util.*", "java.lang.*")); + private static final int DEFAULT_MAX_BODY_LENGTH = 50; private static String bodyEncoding = DEFAULT_ENCODING; + private static int maxBodyLength = DEFAULT_MAX_BODY_LENGTH; + private final MessageProperties messageProperties; private final byte[] body; @@ -78,23 +75,6 @@ public Message(byte[] body, MessageProperties messageProperties) { //NOSONAR this.messageProperties = messageProperties; } - /** - * Add patterns to the allowed list of permissible package/class name patterns for - * deserialization in {@link #toString()}. - * The patterns will be applied in order until a match is found. - * A class can be fully qualified or a wildcard '*' is allowed at the - * beginning or end of the class name. - * Examples: {@code com.foo.*}, {@code *.MyClass}. - * By default, only {@code java.util} and {@code java.lang} classes will be - * deserialized. - * @param patterns the patterns. - * @since 1.5.7 - */ - public static void addAllowedListPatterns(String... patterns) { - Assert.notNull(patterns, "'patterns' cannot be null"); - ALLOWED_LIST_PATTERNS.addAll(Arrays.asList(patterns)); - } - /** * Set the encoding to use in {@link #toString()} when converting the body if * there is no {@link MessageProperties#getContentEncoding() contentEncoding} message property present. @@ -106,6 +86,16 @@ public static void setDefaultEncoding(String encoding) { bodyEncoding = encoding; } + /** + * Set the maximum length of a test message body to render as a String in + * {@link #toString()}. Default 50. + * @param length the length to render. + * @since 2.2.20 + */ + public static void setMaxBodyLength(int length) { + maxBodyLength = length; + } + public byte[] getBody() { return this.body; //NOSONAR } @@ -128,14 +118,14 @@ private String getBodyContentAsString() { try { String contentType = this.messageProperties.getContentType(); if (MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT.equals(contentType)) { - return SerializationUtils.deserialize(new ByteArrayInputStream(this.body), ALLOWED_LIST_PATTERNS, - ClassUtils.getDefaultClassLoader()).toString(); + return "[serialized object]"; } String encoding = encoding(); - if (MessageProperties.CONTENT_TYPE_TEXT_PLAIN.equals(contentType) + if (this.body.length <= maxBodyLength // NOSONAR + && (MessageProperties.CONTENT_TYPE_TEXT_PLAIN.equals(contentType) || MessageProperties.CONTENT_TYPE_JSON.equals(contentType) || MessageProperties.CONTENT_TYPE_JSON_ALT.equals(contentType) - || MessageProperties.CONTENT_TYPE_XML.equals(contentType)) { + || MessageProperties.CONTENT_TYPE_XML.equals(contentType))) { return new String(this.body, encoding); } } @@ -179,14 +169,9 @@ public boolean equals(Object obj) { return false; } if (this.messageProperties == null) { - if (other.messageProperties != null) { - return false; - } - } - else if (!this.messageProperties.equals(other.messageProperties)) { - return false; + return other.messageProperties == null; } - return true; + return this.messageProperties.equals(other.messageProperties); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageBuilderSupport.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageBuilderSupport.java index 16ee6b32b9..e25fa459c0 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageBuilderSupport.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageBuilderSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author 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.Map; import java.util.Map.Entry; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; /** @@ -321,7 +323,7 @@ public MessageBuilderSupport copyHeaders(Map headers) { } public MessageBuilderSupport copyHeadersIfAbsent(Map headers) { - Map existingHeaders = this.properties.getHeaders(); + Map existingHeaders = this.properties.getHeaders(); for (Entry entry : headers.entrySet()) { if (!existingHeaders.containsKey(entry.getKey())) { existingHeaders.put(entry.getKey(), entry.getValue()); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageDeliveryMode.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageDeliveryMode.java index 9c55e1813c..d65ca1111e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageDeliveryMode.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageDeliveryMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,13 @@ /** * Enumeration for the message delivery mode. Can be persistent or - * non persistent. Use the method 'toInt' to get the appropriate value + * non-persistent. Use the method 'toInt' to get the appropriate value * that is used by the AMQP protocol instead of the ordinal() value when * passing into AMQP APIs. * * @author Mark Pollack * @author Gary Russell + * @author Artem Bilan * */ public enum MessageDeliveryMode { @@ -39,25 +40,18 @@ public enum MessageDeliveryMode { PERSISTENT; public static int toInt(MessageDeliveryMode mode) { - switch (mode) { - case NON_PERSISTENT: - return 1; - case PERSISTENT: - return 2; - default: - return -1; - } + return switch (mode) { + case NON_PERSISTENT -> 1; + case PERSISTENT -> 2; + }; } public static MessageDeliveryMode fromInt(int modeAsNumber) { - switch (modeAsNumber) { - case 1: - return NON_PERSISTENT; - case 2: - return PERSISTENT; - default: - return null; - } + return switch (modeAsNumber) { + case 1 -> NON_PERSISTENT; + case 2 -> PERSISTENT; + default -> throw new IllegalArgumentException("Unknown mode: " + modeAsNumber); + }; } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageListener.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageListener.java index e0dbe8397f..34f786dd44 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageListener.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,16 @@ default void containerAckMode(AcknowledgeMode mode) { // NOSONAR - empty } + /** + * Return true if this listener is request/reply and the replies are + * async. + * @return true for async replies. + * @since 2.2.21 + */ + default boolean isAsyncReplies() { + return false; + } + /** * Delivers a batch of messages. * @param messages the messages. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessagePostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessagePostProcessor.java index 5cc27b8ba6..13f29019aa 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessagePostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessagePostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.amqp.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; /** @@ -56,7 +58,7 @@ public interface MessagePostProcessor { * @return the message. * @since 1.6.7 */ - default Message postProcessMessage(Message message, Correlation correlation) { + default Message postProcessMessage(Message message, @Nullable Correlation correlation) { return postProcessMessage(message); } @@ -70,7 +72,9 @@ default Message postProcessMessage(Message message, Correlation correlation) { * @return the message. * @since 2.3.4 */ - default Message postProcessMessage(Message message, Correlation correlation, String exchange, String routingKey) { + default Message postProcessMessage(Message message, @Nullable Correlation correlation, + String exchange, String routingKey) { + return postProcessMessage(message, correlation); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index 6d61db4ab3..08c55e57e6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.amqp.core; +import java.io.Serial; import java.io.Serializable; import java.lang.reflect.Method; import java.lang.reflect.Type; @@ -24,6 +25,10 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + /** * Message Properties for an AMQP message. * @@ -33,11 +38,12 @@ * @author Dmitry Chernyshov * @author Artem Bilan * @author Csaba Soti + * @author Raylax Grey + * @author Ngoc Nhan */ public class MessageProperties implements Serializable { - private static final int INT_MASK = 32; - + @Serial private static final long serialVersionUID = 1619000546531112290L; public static final String CONTENT_TYPE_BYTES = "application/octet-stream"; @@ -60,67 +66,82 @@ public class MessageProperties implements Serializable { public static final String X_DELAY = "x-delay"; + /** + * The custom header to represent a number of retries a message is republished. + * In case of server-side DLX, this header contains the value of {@code x-death.count} property. + * When republish is done manually, this header has to be incremented by the application. + */ + public static final String RETRY_COUNT = "retry-count"; + public static final String DEFAULT_CONTENT_TYPE = CONTENT_TYPE_BYTES; public static final MessageDeliveryMode DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT; public static final Integer DEFAULT_PRIORITY = 0; - private final Map headers = new HashMap<>(); + /** + * The maximum value of x-delay header. + * @since 3.1.2 + */ + public static final long X_DELAY_MAX = 0xffffffffL; + + private final HashMap headers = new HashMap<>(); - private Date timestamp; + private @Nullable Date timestamp; - private String messageId; + private @Nullable String messageId; - private String userId; + private @Nullable String userId; - private String appId; + private @Nullable String appId; - private String clusterId; + private @Nullable String clusterId; - private String type; + private @Nullable String type; - private String correlationId; + private @Nullable String correlationId; - private String replyTo; + private @Nullable String replyTo; private String contentType = DEFAULT_CONTENT_TYPE; - private String contentEncoding; + private @Nullable String contentEncoding; private long contentLength; private boolean contentLengthSet; - private MessageDeliveryMode deliveryMode = DEFAULT_DELIVERY_MODE; + private @Nullable MessageDeliveryMode deliveryMode = DEFAULT_DELIVERY_MODE; - private String expiration; + private @Nullable String expiration; private Integer priority = DEFAULT_PRIORITY; - private Boolean redelivered; + private @Nullable Boolean redelivered; - private String receivedExchange; + private @Nullable String receivedExchange; - private String receivedRoutingKey; + private @Nullable String receivedRoutingKey; - private String receivedUserId; + private @Nullable String receivedUserId; private long deliveryTag; private boolean deliveryTagSet; - private Integer messageCount; + private @Nullable Integer messageCount; // Not included in hashCode() - private String consumerTag; + private @Nullable String consumerTag; - private String consumerQueue; + private @Nullable String consumerQueue; - private Integer receivedDelay; + private @Nullable Long receivedDelay; - private MessageDeliveryMode receivedDeliveryMode; + private @Nullable MessageDeliveryMode receivedDeliveryMode; + + private long retryCount; private boolean finalRetryForMessageWithNoId; @@ -128,16 +149,29 @@ public class MessageProperties implements Serializable { private boolean lastInBatch; - private transient Type inferredArgumentType; + private boolean projectionUsed; + + private transient @Nullable Type inferredArgumentType; - private transient Method targetMethod; + private transient @Nullable Method targetMethod; - private transient Object targetBean; + private transient @Nullable Object targetBean; + + private transient @Nullable AmqpAcknowledgment amqpAcknowledgment; public void setHeader(String key, Object value) { this.headers.put(key, value); } + /** + * Set headers. + * @param headers the headers. + * @since 2.4.7 + */ + public void setHeaders(Map headers) { + this.headers.putAll(headers); + } + /** * Typed getter for a header. * @param headerName the header name. @@ -146,11 +180,11 @@ public void setHeader(String key, Object value) { * @since 2.2 */ @SuppressWarnings("unchecked") - public T getHeader(String headerName) { + public @Nullable T getHeader(String headerName) { return (T) this.headers.get(headerName); } - public Map getHeaders() { + public Map getHeaders() { return this.headers; } @@ -158,20 +192,15 @@ public void setTimestamp(Date timestamp) { this.timestamp = timestamp; //NOSONAR } - // NOTE qpid java timestamp is long, presumably can convert to Date. - public Date getTimestamp() { + public @Nullable Date getTimestamp() { return this.timestamp; //NOSONAR } - // NOTE Not forward compatible with qpid 1.0 .NET - // qpid 0.8 .NET/Java: is a string - // qpid 1.0 .NET: MessageId property on class MessageProperties and is UUID - // There is an 'ID' stored IMessage class and is an int. public void setMessageId(String messageId) { this.messageId = messageId; } - public String getMessageId() { + public @Nullable String getMessageId() { return this.messageId; } @@ -179,10 +208,7 @@ public void setUserId(String userId) { this.userId = userId; } - // NOTE Note forward compatible with qpid 1.0 .NET - // qpid 0.8 .NET/java: is a string - // qpid 1.0 .NET: getUserId is byte[] - public String getUserId() { + public @Nullable String getUserId() { return this.userId; } @@ -191,7 +217,7 @@ public String getUserId() { * @return the user id. * @since 1.6 */ - public String getReceivedUserId() { + public @Nullable String getReceivedUserId() { return this.receivedUserId; } @@ -203,18 +229,15 @@ public void setAppId(String appId) { this.appId = appId; } - public String getAppId() { + public @Nullable String getAppId() { return this.appId; } - // NOTE not forward compatible with qpid 1.0 .NET - // qpid 0.8 .NET/Java: is a string - // qpid 1.0 .NET: is not present public void setClusterId(String clusterId) { this.clusterId = clusterId; } - public String getClusterId() { + public @Nullable String getClusterId() { return this.clusterId; } @@ -222,8 +245,7 @@ public void setType(String type) { this.type = type; } - // NOTE structureType is int in Qpid - public String getType() { + public @Nullable String getType() { return this.type; } @@ -231,7 +253,7 @@ public String getType() { * Set the correlation id. * @param correlationId the id. */ - public void setCorrelationId(String correlationId) { + public void setCorrelationId(@Nullable String correlationId) { this.correlationId = correlationId; } @@ -239,23 +261,23 @@ public void setCorrelationId(String correlationId) { * Get the correlation id. * @return the id. */ - public String getCorrelationId() { + public @Nullable String getCorrelationId() { return this.correlationId; } - public void setReplyTo(String replyTo) { + public void setReplyTo(@Nullable String replyTo) { this.replyTo = replyTo; } - public String getReplyTo() { + public @Nullable String getReplyTo() { return this.replyTo; } - public void setReplyToAddress(Address replyTo) { + public void setReplyToAddress(@Nullable Address replyTo) { this.replyTo = (replyTo != null) ? replyTo.toString() : null; } - public Address getReplyToAddress() { + public @Nullable Address getReplyToAddress() { return (this.replyTo != null) ? new Address(this.replyTo) : null; } @@ -267,11 +289,11 @@ public String getContentType() { return this.contentType; } - public void setContentEncoding(String contentEncoding) { + public void setContentEncoding(@Nullable String contentEncoding) { this.contentEncoding = contentEncoding; } - public String getContentEncoding() { + public @Nullable String getContentEncoding() { return this.contentEncoding; } @@ -288,15 +310,15 @@ protected final boolean isContentLengthSet() { return this.contentLengthSet; } - public void setDeliveryMode(MessageDeliveryMode deliveryMode) { + public void setDeliveryMode(@Nullable MessageDeliveryMode deliveryMode) { this.deliveryMode = deliveryMode; } - public MessageDeliveryMode getDeliveryMode() { + public @Nullable MessageDeliveryMode getDeliveryMode() { return this.deliveryMode; } - public MessageDeliveryMode getReceivedDeliveryMode() { + public @Nullable MessageDeliveryMode getReceivedDeliveryMode() { return this.receivedDeliveryMode; } @@ -304,14 +326,23 @@ public void setReceivedDeliveryMode(MessageDeliveryMode receivedDeliveryMode) { this.receivedDeliveryMode = receivedDeliveryMode; } - // why not a Date or long? + /** + * Set the message expiration. This is a String property per the AMQP 0.9.1 spec. For + * RabbitMQ, this is a String representation of the message time to live in + * milliseconds. + * @param expiration the expiration. + */ public void setExpiration(String expiration) { this.expiration = expiration; } - // NOTE qpid Java broker qpid 0.8/1.0 .NET: is a long. - // 0.8 Spec has: expiration (shortstr) - public String getExpiration() { + /** + * Get the message expiration. This is a String property per the AMQP 0.9.1 spec. For + * RabbitMQ, this is a String representation of the message time to live in + * milliseconds. + * @return the expiration. + */ + public @Nullable String getExpiration() { return this.expiration; } @@ -327,7 +358,7 @@ public void setReceivedExchange(String receivedExchange) { this.receivedExchange = receivedExchange; } - public String getReceivedExchange() { + public @Nullable String getReceivedExchange() { return this.receivedExchange; } @@ -335,7 +366,7 @@ public void setReceivedRoutingKey(String receivedRoutingKey) { this.receivedRoutingKey = receivedRoutingKey; } - public String getReceivedRoutingKey() { + public @Nullable String getReceivedRoutingKey() { return this.receivedRoutingKey; } @@ -343,10 +374,10 @@ public String getReceivedRoutingKey() { * When a delayed message exchange is used the x-delay header on a * received message contains the delay. * @return the received delay. - * @since 1.6 - * @see #getDelay() + * @since 3.1.2 + * @see #getDelayLong() */ - public Integer getReceivedDelay() { + public @Nullable Long getReceivedDelayLong() { return this.receivedDelay; } @@ -354,9 +385,10 @@ public Integer getReceivedDelay() { * When a delayed message exchange is used the x-delay header on a * received message contains the delay. * @param receivedDelay the received delay. - * @since 1.6 + * @since 3.1.2 + * @see #setDelayLong(Long) */ - public void setReceivedDelay(Integer receivedDelay) { + public void setReceivedDelayLong(Long receivedDelay) { this.receivedDelay = receivedDelay; } @@ -364,14 +396,14 @@ public void setRedelivered(Boolean redelivered) { this.redelivered = redelivered; } - public Boolean isRedelivered() { + public @Nullable Boolean isRedelivered() { return this.redelivered; } /* * Additional accessor because is* is not standard for type Boolean */ - public Boolean getRedelivered() { + public @Nullable Boolean getRedelivered() { return this.redelivered; } @@ -402,11 +434,11 @@ public void setMessageCount(Integer messageCount) { * Only applies to messages retrieved via {@code basicGet}. * @return the count. */ - public Integer getMessageCount() { + public @Nullable Integer getMessageCount() { return this.messageCount; } - public String getConsumerTag() { + public @Nullable String getConsumerTag() { return this.consumerTag; } @@ -414,7 +446,7 @@ public void setConsumerTag(String consumerTag) { this.consumerTag = consumerTag; } - public String getConsumerQueue() { + public @Nullable String getConsumerQueue() { return this.consumerQueue; } @@ -423,33 +455,58 @@ public void setConsumerQueue(String consumerQueue) { } /** - * The x-delay header (outbound). + * Get the x-delay header long value. * @return the delay. - * @since 1.6 - * @see #getReceivedDelay() + * @since 3.1.2 */ - public Integer getDelay() { + public @Nullable Long getDelayLong() { Object delay = this.headers.get(X_DELAY); - if (delay instanceof Integer) { - return (Integer) delay; - } - else { - return null; + if (delay instanceof Long delayLong) { + return delayLong; } + return null; } /** - * Set the x-delay header. + * Set the x-delay header to a long value. * @param delay the delay. - * @since 1.6 + * @since 3.1.2 */ - public void setDelay(Integer delay) { + public void setDelayLong(@Nullable Long delay) { if (delay == null || delay < 0) { this.headers.remove(X_DELAY); + return; } - else { - this.headers.put(X_DELAY, delay); - } + + Assert.isTrue(delay <= X_DELAY_MAX, "Delay cannot exceed " + X_DELAY_MAX); + this.headers.put(X_DELAY, delay); + } + + /** + * The number of retries for this message over broker. + * @return the retry count + * @since 3.2 + */ + public long getRetryCount() { + return this.retryCount; + } + + /** + * Set a number of retries for this message over broker. + * @param retryCount the retry count. + * @since 3.2 + * @see #incrementRetryCount() + */ + public void setRetryCount(long retryCount) { + this.retryCount = retryCount; + } + + /** + * Increment a retry count for this message when it is re-published back to the broker. + * @since 3.2 + */ + public void incrementRetryCount() { + this.retryCount++; } public boolean isFinalRetryForMessageWithNoId() { @@ -461,7 +518,7 @@ public void setFinalRetryForMessageWithNoId(boolean finalRetryForMessageWithNoId } /** - * Return the publish sequence number if publisher confirms are enabled; set by the template. + * Return the publishing sequence number if publisher confirms are enabled; set by the template. * @return the sequence number. * @since 2.1 */ @@ -470,7 +527,7 @@ public long getPublishSequenceNumber() { } /** - * Set the publish sequence number, if publisher confirms are enabled; set by the template. + * Set the publishing sequence number, if publisher confirms are enabled; set by the template. * @param publishSequenceNumber the sequence number. * @since 2.1 */ @@ -484,7 +541,7 @@ public void setPublishSequenceNumber(long publishSequenceNumber) { * @return the type. * @since 1.6 */ - public Type getInferredArgumentType() { + public @Nullable Type getInferredArgumentType() { return this.inferredArgumentType; } @@ -503,7 +560,7 @@ public void setInferredArgumentType(Type inferredArgumentType) { * @return the method. * @since 1.6 */ - public Method getTargetMethod() { + public @Nullable Method getTargetMethod() { return this.targetMethod; } @@ -512,7 +569,7 @@ public Method getTargetMethod() { * @param targetMethod the target method. * @since 1.6 */ - public void setTargetMethod(Method targetMethod) { + public void setTargetMethod(@Nullable Method targetMethod) { this.targetMethod = targetMethod; } @@ -521,7 +578,7 @@ public void setTargetMethod(Method targetMethod) { * @return the bean. * @since 1.6 */ - public Object getTargetBean() { + public @Nullable Object getTargetBean() { return this.targetBean; } @@ -530,7 +587,7 @@ public Object getTargetBean() { * @param targetBean the bean. * @since 1.6 */ - public void setTargetBean(Object targetBean) { + public void setTargetBean(@Nullable Object targetBean) { this.targetBean = targetBean; } @@ -552,12 +609,32 @@ public void setLastInBatch(boolean lastInBatch) { this.lastInBatch = lastInBatch; } + /** + * Get an internal flag used to communicate that conversion used projection; always + * false at the application level. + * @return true if projection was used. + * @since 2.2.20 + */ + public boolean isProjectionUsed() { + return this.projectionUsed; + } + + /** + * Set an internal flag used to communicate that conversion used projection; always false + * at the application level. + * @param projectionUsed true for projection. + * @since 2.2.20 + */ + public void setProjectionUsed(boolean projectionUsed) { + this.projectionUsed = projectionUsed; + } + /** * Return the x-death header. * @return the header. */ @SuppressWarnings("unchecked") - public List> getXDeathHeader() { + public @Nullable List> getXDeathHeader() { try { return (List>) this.headers.get("x-death"); } @@ -566,6 +643,25 @@ public void setLastInBatch(boolean lastInBatch) { } } + /** + * Return the {@link AmqpAcknowledgment} for consumer if any. + * @return the {@link AmqpAcknowledgment} for consumer if any. + * @since 4.0 + */ + public @Nullable AmqpAcknowledgment getAmqpAcknowledgment() { + return this.amqpAcknowledgment; + } + + /** + * Set an {@link AmqpAcknowledgment} for manual acks in the target message processor. + * This is only in-application a consumer side logic. + * @param amqpAcknowledgment the {@link AmqpAcknowledgment} to use in the application. + * @since 4.0 + */ + public void setAmqpAcknowledgment(AmqpAcknowledgment amqpAcknowledgment) { + this.amqpAcknowledgment = amqpAcknowledgment; + } + @Override // NOSONAR complexity public int hashCode() { final int prime = 31; @@ -573,16 +669,16 @@ public int hashCode() { result = prime * result + ((this.appId == null) ? 0 : this.appId.hashCode()); result = prime * result + ((this.clusterId == null) ? 0 : this.clusterId.hashCode()); result = prime * result + ((this.contentEncoding == null) ? 0 : this.contentEncoding.hashCode()); - result = prime * result + (int) (this.contentLength ^ (this.contentLength >>> INT_MASK)); - result = prime * result + ((this.contentType == null) ? 0 : this.contentType.hashCode()); + result = prime * result + Long.hashCode(this.contentLength); + result = prime * result + this.contentType.hashCode(); result = prime * result + ((this.correlationId == null) ? 0 : this.correlationId.hashCode()); result = prime * result + ((this.deliveryMode == null) ? 0 : this.deliveryMode.hashCode()); - result = prime * result + (int) (this.deliveryTag ^ (this.deliveryTag >>> INT_MASK)); + result = prime * result + Long.hashCode(this.deliveryTag); result = prime * result + ((this.expiration == null) ? 0 : this.expiration.hashCode()); result = prime * result + this.headers.hashCode(); result = prime * result + ((this.messageCount == null) ? 0 : this.messageCount.hashCode()); result = prime * result + ((this.messageId == null) ? 0 : this.messageId.hashCode()); - result = prime * result + ((this.priority == null) ? 0 : this.priority.hashCode()); + result = prime * result + this.priority.hashCode(); result = prime * result + ((this.receivedExchange == null) ? 0 : this.receivedExchange.hashCode()); result = prime * result + ((this.receivedRoutingKey == null) ? 0 : this.receivedRoutingKey.hashCode()); result = prime * result + ((this.redelivered == null) ? 0 : this.redelivered.hashCode()); @@ -632,12 +728,7 @@ else if (!this.contentEncoding.equals(other.contentEncoding)) { if (this.contentLength != other.contentLength) { return false; } - if (this.contentType == null) { - if (other.contentType != null) { - return false; - } - } - else if (!this.contentType.equals(other.contentType)) { + if (!this.contentType.equals(other.contentType)) { return false; } @@ -683,12 +774,7 @@ else if (!this.messageCount.equals(other.messageCount)) { else if (!this.messageId.equals(other.messageId)) { return false; } - if (this.priority == null) { - if (other.priority != null) { - return false; - } - } - else if (!this.priority.equals(other.priority)) { + if (!this.priority.equals(other.priority)) { return false; } if (this.receivedExchange == null) { @@ -740,14 +826,9 @@ else if (!this.type.equals(other.type)) { return false; } if (this.userId == null) { - if (other.userId != null) { - return false; - } - } - else if (!this.userId.equals(other.userId)) { - return false; + return other.userId == null; } - return true; + return this.userId.equals(other.userId); } @Override // NOSONAR complexity @@ -762,13 +843,13 @@ public String toString() { + (this.type == null ? "" : ", type=" + this.type) + (this.correlationId == null ? "" : ", correlationId=" + this.correlationId) + (this.replyTo == null ? "" : ", replyTo=" + this.replyTo) - + (this.contentType == null ? "" : ", contentType=" + this.contentType) + + ", contentType=" + this.contentType + (this.contentEncoding == null ? "" : ", contentEncoding=" + this.contentEncoding) + ", contentLength=" + this.contentLength + (this.deliveryMode == null ? "" : ", deliveryMode=" + this.deliveryMode) + (this.receivedDeliveryMode == null ? "" : ", receivedDeliveryMode=" + this.receivedDeliveryMode) + (this.expiration == null ? "" : ", expiration=" + this.expiration) - + (this.priority == null ? "" : ", priority=" + this.priority) + + ", priority=" + this.priority + (this.redelivered == null ? "" : ", redelivered=" + this.redelivered) + (this.receivedExchange == null ? "" : ", receivedExchange=" + this.receivedExchange) + (this.receivedRoutingKey == null ? "" : ", receivedRoutingKey=" + this.receivedRoutingKey) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java index 5cebaa1990..a6fd51a101 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,8 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -28,18 +29,11 @@ * * @author Mark Pollack * @author Gary Russell + * * @see AmqpAdmin */ public class Queue extends AbstractDeclarable implements Cloneable { - /** - * Argument key for the master locator. - * @since 2.1 - * @deprecated in favor of {@link #X_QUEUE_LEADER_LOCATOR}. - */ - @Deprecated - public static final String X_QUEUE_MASTER_LOCATOR = "x-queue-master-locator"; - /** * Argument key for the queue leader locator. * @since 2.1 @@ -97,7 +91,7 @@ public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete * @param arguments the arguments used to declare the queue */ public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, - @Nullable Map arguments) { + @Nullable Map arguments) { super(arguments); Assert.notNull(name, "'name' cannot be null"); @@ -165,22 +159,6 @@ public String getActualName() { return this.actualName; } - /** - * Set the master locator strategy argument for this queue. - * @param locator the locator; null to clear the argument. - * @since 2.1 - * @deprecated in favor of {@link #setLeaderLocator(String)}. - */ - @Deprecated - public final void setMasterLocator(@Nullable String locator) { - if (locator == null) { - removeArgument(X_QUEUE_LEADER_LOCATOR); - } - else { - addArgument(X_QUEUE_LEADER_LOCATOR, locator); - } - } - /** * Set the leader locator strategy argument for this queue. * @param locator the locator; null to clear the argument. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java index f76bbdb9d8..e2cc82c9af 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -149,10 +149,10 @@ public QueueBuilder expires(int expires) { * them. * @param count the number of (ready) messages allowed. * @return the builder. - * @since 2.2 + * @since 3.1 * @see #overflow(Overflow) */ - public QueueBuilder maxLength(int count) { + public QueueBuilder maxLength(long count) { return withArgument("x-max-length", count); } @@ -226,22 +226,7 @@ public QueueBuilder lazy() { /** * Set the master locator mode which determines which node a queue master will be * located on a cluster of nodes. - * @param locator {@link MasterLocator#minMasters}, {@link MasterLocator#clientLocal} - * or {@link MasterLocator#random}. - * @return the builder. - * @since 2.2 - * @deprecated in favor of {@link #leaderLocator(LeaderLocator)}. - */ - @Deprecated - public QueueBuilder masterLocator(MasterLocator locator) { - return withArgument("x-queue-master-locator", locator.getValue()); - } - - /** - * Set the master locator mode which determines which node a queue master will be - * located on a cluster of nodes. - * @param locator {@link MasterLocator#minMasters}, {@link MasterLocator#clientLocal} - * or {@link MasterLocator#random}. + * @param locator {@link LeaderLocator}. * @return the builder. * @since 2.2 */ @@ -326,43 +311,6 @@ public String getValue() { } - /** - * @deprecated in favor of {@link LeaderLocator}. - */ - @Deprecated - public enum MasterLocator { - - /** - * Deploy on the node with the fewest masters. - */ - minMasters("min-masters"), - - /** - * Deploy on the node we are connected to. - */ - clientLocal("client-local"), - - /** - * Deploy on a random node. - */ - random("random"); - - private final String value; - - MasterLocator(String value) { - this.value = value; - } - - /** - * Return the value. - * @return the value. - */ - public String getValue() { - return this.value; - } - - } - /** * Locate the queue leader. * diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java index de914f9439..2323774978 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author 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,9 @@ * Information about a queue, resulting from a passive declaration. * * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.2 * */ @@ -27,11 +30,13 @@ public class QueueInformation { private final String name; - private final int messageCount; + private final long messageCount; private final int consumerCount; - public QueueInformation(String name, int messageCount, int consumerCount) { + private String type = "classic"; + + public QueueInformation(String name, long messageCount, int consumerCount) { this.name = name; this.messageCount = messageCount; this.consumerCount = consumerCount; @@ -41,7 +46,7 @@ public String getName() { return this.name; } - public int getMessageCount() { + public long getMessageCount() { return this.messageCount; } @@ -49,11 +54,30 @@ public int getConsumerCount() { return this.consumerCount; } + /** + * Return a queue type. + * {@code classic} by default since AMQP 0.9.1 protocol does not return this info in {@code DeclareOk} reply. + * @return a queue type + * @since 4.0 + */ + public String getType() { + return this.type; + } + + /** + * Set a queue type. + * @param type the queue type: {@code quorum}, {@code classic} or {@code stream} + * @since 4.0 + */ + public void setType(String type) { + this.type = type; + } + @Override public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); + result = prime * result + this.name.hashCode(); return result; } @@ -69,15 +93,7 @@ public boolean equals(Object obj) { return false; } QueueInformation other = (QueueInformation) obj; - if (this.name == null) { - if (other.name != null) { - return false; - } - } - else if (!this.name.equals(other.name)) { - return false; - } - return true; + return this.name.equals(other.name); } @Override diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/ReceiveAndReplyCallback.java b/spring-amqp/src/main/java/org/springframework/amqp/core/ReceiveAndReplyCallback.java index b3e11af821..0d8ba89cce 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/ReceiveAndReplyCallback.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/ReceiveAndReplyCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,25 @@ package org.springframework.amqp.core; +import org.jspecify.annotations.Nullable; + /** * To be used with the receive-and-reply methods of {@link org.springframework.amqp.core.AmqpTemplate} * as processor for inbound object and producer for outbound object. * *

This often as an anonymous class within a method implementation. - * @param The type of the request after conversion from the {@link Message}. * @param The type of the response. * * @author Artem Bilan * @author Gary Russell + * * @since 1.3 */ @FunctionalInterface public interface ReceiveAndReplyCallback { + @Nullable S handle(R payload); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/TopicExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/TopicExchange.java index fdac8366d9..114295e6d5 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/TopicExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/TopicExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,16 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Simple container collecting information to describe a topic exchange. * Used in conjunction with administrative operations. * * @author Mark Pollack * @author Dave Syer + * @author Artem Bilan + * * @see AmqpAdmin */ public class TopicExchange extends AbstractExchange { @@ -36,7 +40,9 @@ public TopicExchange(String name, boolean durable, boolean autoDelete) { super(name, durable, autoDelete); } - public TopicExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public TopicExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/core/package-info.java index e04f829468..fea457ea01 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/package-info.java @@ -1,4 +1,5 @@ /** * Provides core classes for the spring AMQP abstraction. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.core; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/event/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/event/package-info.java index 5429f0253d..133bb99607 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/event/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/event/package-info.java @@ -1,4 +1,5 @@ /** * Classes related to application events */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.event; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/package-info.java index 0dfcd63dc9..9e2970f6d9 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/package-info.java @@ -1,4 +1,5 @@ /** * Base package for Spring AMQP. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java deleted file mode 100644 index 0537060730..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.remoting.client; - -import java.util.Arrays; - -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; - -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.remoting.RemoteProxyFailureException; -import org.springframework.remoting.support.DefaultRemoteInvocationFactory; -import org.springframework.remoting.support.RemoteAccessor; -import org.springframework.remoting.support.RemoteInvocation; -import org.springframework.remoting.support.RemoteInvocationFactory; -import org.springframework.remoting.support.RemoteInvocationResult; - -/** - * {@link org.aopalliance.intercept.MethodInterceptor} for accessing RMI-style AMQP services. - * - * @author David Bilge - * @author Gary Russell - * @since 1.2 - * @see org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter - * @see AmqpProxyFactoryBean - * @see org.springframework.remoting.RemoteAccessException - */ -public class AmqpClientInterceptor extends RemoteAccessor implements MethodInterceptor { - - private AmqpTemplate amqpTemplate; - - private String routingKey = null; - - private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory(); - - @Override - public Object invoke(MethodInvocation invocation) throws Throwable { - RemoteInvocation remoteInvocation = getRemoteInvocationFactory().createRemoteInvocation(invocation); - - Object rawResult; - if (getRoutingKey() == null) { - // Use the template's default routing key - rawResult = this.amqpTemplate.convertSendAndReceive(remoteInvocation); - } - else { - rawResult = this.amqpTemplate.convertSendAndReceive(this.routingKey, remoteInvocation); - } - - if (rawResult == null) { - throw new RemoteProxyFailureException("No reply received from '" + - remoteInvocation.getMethodName() + - "' with arguments '" + - Arrays.asList(remoteInvocation.getArguments()) + // NOSONAR (null) - "' - perhaps a timeout in the template?", null); - } - else if (!(rawResult instanceof RemoteInvocationResult)) { - throw new RemoteProxyFailureException("Expected a result of type " - + RemoteInvocationResult.class.getCanonicalName() + " but found " - + rawResult.getClass().getCanonicalName(), null); // NOSONAR (null) - } - - RemoteInvocationResult result = (RemoteInvocationResult) rawResult; - return result.recreate(); - } - - public AmqpTemplate getAmqpTemplate() { - return this.amqpTemplate; - } - - /** - * The AMQP template to be used for sending messages and receiving results. This class is using "Request/Reply" for - * sending messages as described in the Spring-AMQP - * documentation. - * - * @param amqpTemplate The amqp template. - */ - public void setAmqpTemplate(AmqpTemplate amqpTemplate) { - this.amqpTemplate = amqpTemplate; - } - - public String getRoutingKey() { - return this.routingKey; - } - - /** - * The routing key to send calls to the service with. Use this to route the messages to a specific queue on the - * broker. If not set, the {@link AmqpTemplate}'s default routing key will be used. - *

- * This property is useful if you want to use the same AmqpTemplate to talk to multiple services. - * - * @param routingKey The routing key. - */ - public void setRoutingKey(String routingKey) { - this.routingKey = routingKey; - } - - public RemoteInvocationFactory getRemoteInvocationFactory() { - return this.remoteInvocationFactory; - } - - /** - * Set the RemoteInvocationFactory to use for this accessor. Default is a {@link DefaultRemoteInvocationFactory}. - *

- * A custom invocation factory can add further context information to the invocation, for example user credentials. - * - * @param remoteInvocationFactory The remote invocation factory. - */ - public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) { - this.remoteInvocationFactory = remoteInvocationFactory; - } - -} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java deleted file mode 100644 index d63ddce60e..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.remoting.client; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; - -/** - * {@link FactoryBean} for AMQP proxies. Exposes the proxied service for use as a bean reference, using the specified - * service interface. Proxies will throw Spring's unchecked RemoteAccessException on remote invocation failure. - * - *

- * This is intended for an "RMI-style" (i.e. synchroneous) usage of the AMQP protocol. Obviously, AMQP allows for a much - * broader scope of execution styles, which are not the scope of the mechanism at hand. - *

- * Calling a method on the proxy will cause an AMQP message being sent according to the configured - * {@link org.springframework.amqp.core.AmqpTemplate}. - * This can be received and answered by an {@link org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter}. - * - * @author David Bilge - * @author Gary Russell - * - * @since 1.2 - * @see #setServiceInterface - * @see AmqpClientInterceptor - * @see org.springframework.remoting.rmi.RmiServiceExporter - * @see org.springframework.remoting.RemoteAccessException - */ -public class AmqpProxyFactoryBean extends AmqpClientInterceptor implements FactoryBean, InitializingBean { - - private Object serviceProxy; - - @Override - public void afterPropertiesSet() { - if (getServiceInterface() == null) { - throw new IllegalArgumentException("Property 'serviceInterface' is required"); - } - this.serviceProxy = new ProxyFactory(getServiceInterface(), this).getProxy(getBeanClassLoader()); - } - - @Override - public Object getObject() { - return this.serviceProxy; - } - - @Override - public Class getObjectType() { - return getServiceInterface(); - } - - @Override - public boolean isSingleton() { - return true; - } - -} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/package-info.java deleted file mode 100644 index ac8ebec8ef..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Provides classes for the client side of Spring Remoting over AMQP. - */ -package org.springframework.amqp.remoting.client; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java deleted file mode 100644 index ca14839e87..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.remoting.service; - -import org.springframework.amqp.AmqpRejectAndDontRequeueException; -import org.springframework.amqp.core.Address; -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageListener; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.amqp.support.converter.SimpleMessageConverter; -import org.springframework.remoting.support.RemoteInvocation; -import org.springframework.remoting.support.RemoteInvocationBasedExporter; -import org.springframework.remoting.support.RemoteInvocationResult; - -/** - * This message listener exposes a plain java service via AMQP. Such services can be accessed via plain AMQP or via - * {@link org.springframework.amqp.remoting.client.AmqpProxyFactoryBean}. - * - * To configure this message listener so that it actually receives method calls via AMQP, it needs to be put into a - * listener container. See {@link MessageListener}. - * - *

- * When receiving a message, a service method is called according to the contained {@link RemoteInvocation}. The result - * of that invocation is returned as a {@link RemoteInvocationResult} contained in a message that is sent according to - * the ReplyToAddress of the received message. - * - *

- * Please note that this exporter does not use the {@link MessageConverter} of the injected {@link AmqpTemplate} to - * convert incoming calls and their results. Instead you have to directly inject the MessageConverter into - * this class. - * - *

- * This listener responds to "Request/Reply"-style messages as described here. - * - * @author David Bilge - * @author Gary Russell - * @author Artem Bilan - * @since 1.2 - */ -public class AmqpInvokerServiceExporter extends RemoteInvocationBasedExporter implements MessageListener { - - private AmqpTemplate amqpTemplate; - - private MessageConverter messageConverter = new SimpleMessageConverter(); - - @Override - public void onMessage(Message message) { - Address replyToAddress = message.getMessageProperties().getReplyToAddress(); - if (replyToAddress == null) { - throw new AmqpRejectAndDontRequeueException("No replyToAddress in inbound AMQP Message"); - } - - Object invocationRaw = this.messageConverter.fromMessage(message); - - RemoteInvocationResult remoteInvocationResult; - if (!(invocationRaw instanceof RemoteInvocation)) { - remoteInvocationResult = new RemoteInvocationResult( - new IllegalArgumentException("The message does not contain a RemoteInvocation payload")); - } - else { - RemoteInvocation invocation = (RemoteInvocation) invocationRaw; - remoteInvocationResult = invokeAndCreateResult(invocation, getService()); - } - send(remoteInvocationResult, replyToAddress, message); - } - - private void send(Object object, Address replyToAddress, Message requestMessage) { - Message message = this.messageConverter.toMessage(object, new MessageProperties()); - message.getMessageProperties().setCorrelationId(requestMessage.getMessageProperties().getCorrelationId()); - - getAmqpTemplate().send(replyToAddress.getExchangeName(), replyToAddress.getRoutingKey(), message); - } - - public AmqpTemplate getAmqpTemplate() { - return this.amqpTemplate; - } - - /** - * The AMQP template to use for sending the return value. - * - *

- * Note that the exchange and routing key parameters on this template are ignored for these return messages. Instead - * of those the respective parameters from the original message's returnAddress are being used. - *

- * Also, the template's {@link MessageConverter} is not used for the reply. - * - * @param amqpTemplate The amqp template. - * - * @see #setMessageConverter(MessageConverter) - */ - public void setAmqpTemplate(AmqpTemplate amqpTemplate) { - this.amqpTemplate = amqpTemplate; - } - - public MessageConverter getMessageConverter() { - return this.messageConverter; - } - - /** - * Set the message converter for this remote service. Used to deserialize remote method calls and to serialize their - * return values. - *

- * The default converter is a SimpleMessageConverter, which is able to handle byte arrays, Strings, and Serializable - * Objects depending on the message content type header. - *

- * Note that this class never uses the message converter of the underlying {@link AmqpTemplate}! - * - * @param messageConverter The message converter. - * - * @see org.springframework.amqp.support.converter.SimpleMessageConverter - */ - public void setMessageConverter(MessageConverter messageConverter) { - this.messageConverter = messageConverter; - } - -} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/package-info.java deleted file mode 100644 index 5ec67d1a13..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Provides classes for the service side of Spring Remoting over AMQP. - */ -package org.springframework.amqp.remoting.service; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpHeaders.java b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpHeaders.java index a4f64b6475..5478b48573 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpHeaders.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpHeaders.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. @@ -138,4 +138,10 @@ public abstract class AmqpHeaders { */ public static final String BATCH_SIZE = PREFIX + "batchSize"; + /** + * The number of retries for the message over server republishing. + * @since 3.2 + */ + public static final String RETRY_COUNT = PREFIX + "retryCount"; + } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java index 09ca52a90b..05f9ae5a73 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author 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.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.messaging.Message; import org.springframework.messaging.support.NativeMessageHeaderAccessor; @@ -32,6 +34,7 @@ * * @author Stephane Nicoll * @author Gary Russell + * @author Artem Bilan * * @since 1.4 */ @@ -60,7 +63,8 @@ public static AmqpMessageHeaderAccessor wrap(Message message) { } @Override - protected void verifyType(String headerName, Object headerValue) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected void verifyType(@Nullable String headerName, @Nullable Object headerValue) { super.verifyType(headerName, headerValue); if (PRIORITY.equals(headerName)) { Assert.isTrue(Integer.class.isAssignableFrom(headerValue.getClass()), "The '" + headerName @@ -68,87 +72,85 @@ protected void verifyType(String headerName, Object headerValue) { } } - public String getAppId() { + public @Nullable String getAppId() { return (String) getHeader(AmqpHeaders.APP_ID); } - public String getClusterId() { + public @Nullable String getClusterId() { return (String) getHeader(AmqpHeaders.CLUSTER_ID); } - public String getContentEncoding() { + public @Nullable String getContentEncoding() { return (String) getHeader(AmqpHeaders.CONTENT_ENCODING); } - public Long getContentLength() { + public @Nullable Long getContentLength() { return (Long) getHeader(AmqpHeaders.CONTENT_LENGTH); } @Override - public MimeType getContentType() { + public @Nullable MimeType getContentType() { Object value = getHeader(AmqpHeaders.CONTENT_TYPE); - if (value instanceof String) { - return MimeType.valueOf((String) value); - } - else { - return super.getContentType(); + if (value instanceof String contentType) { + return MimeType.valueOf(contentType); } + return super.getContentType(); } - public String getCorrelationId() { + public @Nullable String getCorrelationId() { return (String) getHeader(AmqpHeaders.CORRELATION_ID); } - public MessageDeliveryMode getDeliveryMode() { + public @Nullable MessageDeliveryMode getDeliveryMode() { return (MessageDeliveryMode) getHeader(AmqpHeaders.DELIVERY_MODE); } - public MessageDeliveryMode getReceivedDeliveryMode() { + public @Nullable MessageDeliveryMode getReceivedDeliveryMode() { return (MessageDeliveryMode) getHeader(AmqpHeaders.RECEIVED_DELIVERY_MODE); } - public Long getDeliveryTag() { + public @Nullable Long getDeliveryTag() { return (Long) getHeader(AmqpHeaders.DELIVERY_TAG); } - public String getExpiration() { + public @Nullable String getExpiration() { return (String) getHeader(AmqpHeaders.EXPIRATION); } - public Integer getMessageCount() { + public @Nullable Integer getMessageCount() { return (Integer) getHeader(AmqpHeaders.MESSAGE_COUNT); } - public String getMessageId() { + public @Nullable String getMessageId() { return (String) getHeader(AmqpHeaders.MESSAGE_ID); } - public Integer getPriority() { + public @Nullable Integer getPriority() { return (Integer) getHeader(PRIORITY); } - public String getReceivedExchange() { + public @Nullable String getReceivedExchange() { return (String) getHeader(AmqpHeaders.RECEIVED_EXCHANGE); } - public String getReceivedRoutingKey() { + public @Nullable String getReceivedRoutingKey() { return (String) getHeader(AmqpHeaders.RECEIVED_ROUTING_KEY); } - public String getReceivedUserId() { + public @Nullable String getReceivedUserId() { return (String) getHeader(AmqpHeaders.RECEIVED_USER_ID); } - public Boolean getRedelivered() { + public @Nullable Boolean getRedelivered() { return (Boolean) getHeader(AmqpHeaders.REDELIVERED); } - public String getReplyTo() { + public @Nullable String getReplyTo() { return (String) getHeader(AmqpHeaders.REPLY_TO); } @Override - public Long getTimestamp() { + public @Nullable Long getTimestamp() { Date amqpTimestamp = (Date) getHeader(AmqpHeaders.TIMESTAMP); if (amqpTimestamp != null) { return amqpTimestamp.getTime(); @@ -158,19 +160,19 @@ public Long getTimestamp() { } } - public String getType() { + public @Nullable String getType() { return (String) getHeader(AmqpHeaders.TYPE); } - public String getUserId() { + public @Nullable String getUserId() { return (String) getHeader(AmqpHeaders.USER_ID); } - public String getConsumerTag() { + public @Nullable String getConsumerTag() { return (String) getHeader(AmqpHeaders.CONSUMER_TAG); } - public String getConsumerQueue() { + public @Nullable String getConsumerQueue() { return (String) getHeader(AmqpHeaders.CONSUMER_QUEUE); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java b/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java index f6845d7c87..26faaf721a 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author 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.amqp.support; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.log.LogMessage; /** * For components that support customization of the logging of certain events, users can @@ -35,6 +38,16 @@ public interface ConditionalExceptionLogger { * @param message a message that the caller suggests should be included in the log. * @param t a throwable; may be null. */ - void log(Log logger, String message, Throwable t); + void log(Log logger, String message, @Nullable Throwable t); + + /** + * Log a consumer restart; debug by default. + * @param logger the logger. + * @param message the message. + * @since 3.1 + */ + default void logRestart(Log logger, LogMessage message) { + logger.debug(message); + } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/SendRetryContextAccessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/SendRetryContextAccessor.java index 00542bd2cc..92ebdd0e0b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/SendRetryContextAccessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/SendRetryContextAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or 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.amqp.support; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Address; import org.springframework.amqp.core.Message; import org.springframework.retry.RetryContext; @@ -48,7 +50,7 @@ private SendRetryContextAccessor() { * @return the message. * @see #MESSAGE */ - public static Message getMessage(RetryContext context) { + public static @Nullable Message getMessage(RetryContext context) { return (Message) context.getAttribute(MESSAGE); } @@ -58,7 +60,7 @@ public static Message getMessage(RetryContext context) { * @return the address. * @see #ADDRESS */ - public static Address getAddress(RetryContext context) { + public static @Nullable Address getAddress(RetryContext context) { return (Address) context.getAttribute(ADDRESS); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java index 3031d3d87c..7865cc5388 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.BiConsumer; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.utils.JavaUtils; @@ -48,6 +50,8 @@ * @author Gary Russell * @author Artem Bilan * @author Stephane Nicoll + * @author Raylax Grey + * @author Ngoc Nhan * @since 1.4 */ public class SimpleAmqpHeaderMapper extends AbstractHeaderMapper implements AmqpHeaderMapper { @@ -65,12 +69,12 @@ public void fromHeaders(MessageHeaders headers, MessageProperties amqpMessagePro amqpMessageProperties::setContentLength) .acceptIfHasText(extractContentTypeAsString(headers), amqpMessageProperties::setContentType); Object correlationId = headers.get(AmqpHeaders.CORRELATION_ID); - if (correlationId instanceof String) { - amqpMessageProperties.setCorrelationId((String) correlationId); + if (correlationId instanceof String string) { + amqpMessageProperties.setCorrelationId(string); } javaUtils - .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.DELAY, Integer.class), - amqpMessageProperties::setDelay) + .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.DELAY, Long.class), + amqpMessageProperties::setDelayLong) .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.DELIVERY_MODE, MessageDeliveryMode.class), amqpMessageProperties::setDeliveryMode) .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.DELIVERY_TAG, Long.class), @@ -95,6 +99,8 @@ public void fromHeaders(MessageHeaders headers, MessageProperties amqpMessagePro amqpMessageProperties::setTimestamp) .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.TYPE, String.class), amqpMessageProperties::setType) + .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.RETRY_COUNT, Long.class), + amqpMessageProperties::setRetryCount) .acceptIfHasText(getHeaderIfAvailable(headers, AmqpHeaders.USER_ID, String.class), amqpMessageProperties::setUserId); @@ -112,11 +118,9 @@ public void fromHeaders(MessageHeaders headers, MessageProperties amqpMessagePro String headerName = entry.getKey(); if (StringUtils.hasText(headerName) && !headerName.startsWith(AmqpHeaders.PREFIX)) { Object value = entry.getValue(); - if (value != null) { - String propertyName = this.fromHeaderName(headerName); - if (!amqpMessageProperties.getHeaders().containsKey(headerName)) { - amqpMessageProperties.setHeader(propertyName, value); - } + String propertyName = this.fromHeaderName(headerName); + if (!amqpMessageProperties.getHeaders().containsKey(headerName)) { + amqpMessageProperties.setHeader(propertyName, value); } } } @@ -124,7 +128,7 @@ public void fromHeaders(MessageHeaders headers, MessageProperties amqpMessagePro @Override public MessageHeaders toHeaders(MessageProperties amqpMessageProperties) { - Map headers = new HashMap(); + Map headers = new HashMap<>(); try { BiConsumer putObject = headers::put; BiConsumer putString = headers::put; @@ -148,9 +152,9 @@ public MessageHeaders toHeaders(MessageProperties amqpMessageProperties) { .acceptIfNotNull(AmqpHeaders.MESSAGE_ID, amqpMessageProperties.getMessageId(), putObject); Integer priority = amqpMessageProperties.getPriority(); javaUtils - .acceptIfCondition(priority != null && priority > 0, AmqpMessageHeaderAccessor.PRIORITY, priority, + .acceptIfCondition(priority > 0, AmqpMessageHeaderAccessor.PRIORITY, priority, putObject) - .acceptIfNotNull(AmqpHeaders.RECEIVED_DELAY, amqpMessageProperties.getReceivedDelay(), putObject) + .acceptIfNotNull(AmqpHeaders.RECEIVED_DELAY, amqpMessageProperties.getReceivedDelayLong(), putObject) .acceptIfHasText(AmqpHeaders.RECEIVED_EXCHANGE, amqpMessageProperties.getReceivedExchange(), putString) .acceptIfHasText(AmqpHeaders.RECEIVED_ROUTING_KEY, amqpMessageProperties.getReceivedRoutingKey(), @@ -164,11 +168,10 @@ public MessageHeaders toHeaders(MessageProperties amqpMessageProperties) { .acceptIfHasText(AmqpHeaders.CONSUMER_TAG, amqpMessageProperties.getConsumerTag(), putString) .acceptIfHasText(AmqpHeaders.CONSUMER_QUEUE, amqpMessageProperties.getConsumerQueue(), putString); headers.put(AmqpHeaders.LAST_IN_BATCH, amqpMessageProperties.isLastInBatch()); + headers.put(AmqpHeaders.RETRY_COUNT, amqpMessageProperties.getRetryCount()); // Map custom headers - for (Map.Entry entry : amqpMessageProperties.getHeaders().entrySet()) { - headers.put(entry.getKey(), entry.getValue()); - } + headers.putAll(amqpMessageProperties.getHeaders()); } catch (Exception e) { if (logger.isWarnEnabled()) { @@ -185,7 +188,7 @@ public MessageHeaders toHeaders(MessageProperties amqpMessageProperties) { * @param headers the headers. * @return the content type. */ - private String extractContentTypeAsString(Map headers) { + private @Nullable String extractContentTypeAsString(Map headers) { String contentTypeStringValue = null; Object contentType = getHeaderIfAvailable(headers, AmqpHeaders.CONTENT_TYPE, Object.class); @@ -194,8 +197,8 @@ private String extractContentTypeAsString(Map headers) { if (contentType instanceof MimeType) { contentTypeStringValue = contentType.toString(); } - else if (contentType instanceof String) { - contentTypeStringValue = (String) contentType; + else if (contentType instanceof String string) { + contentTypeStringValue = string; } else { if (logger.isWarnEnabled()) { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java index d0f1460d82..242e09446c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 the original author or authors. + * Copyright 2018-2025 the original author 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,21 +21,22 @@ import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Optional; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; - -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.util.MimeTypeUtils; /** * Abstract Jackson2 message converter. @@ -61,28 +62,32 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + protected final ObjectMapper objectMapper; // NOSONAR protected + /** - * The supported content type; only the subtype is checked, e.g. */json, - * */xml. + * The supported content type; only the subtype is checked when decoding, e.g. + * */json, */xml. If this contains a charset parameter, when encoding, the + * contentType header will not be set, when decoding, the raw bytes are passed to + * Jackson which can dynamically determine the encoding; otherwise the contentEncoding + * or default charset is used. */ - private final MimeType supportedContentType; + private MimeType supportedContentType; - protected final ObjectMapper objectMapper; // NOSONAR protected + private @Nullable String supportedCTCharset; - @Nullable - private ClassMapper classMapper = null; + private @Nullable ClassMapper classMapper = null; private Charset defaultCharset = DEFAULT_CHARSET; private boolean typeMapperSet; - private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); private Jackson2JavaTypeMapper javaTypeMapper = new DefaultJackson2JavaTypeMapper(); private boolean useProjectionForInterfaces; - private ProjectingMessageConverter projectingConverter; + private @Nullable ProjectingMessageConverter projectingConverter; private boolean charsetIsUtf8 = true; @@ -90,11 +95,16 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo private boolean alwaysConvertToInferredType; + private boolean nullAsOptionalEmpty; + /** * Construct with the provided {@link ObjectMapper} instance. * @param objectMapper the {@link ObjectMapper} to use. - * @param contentType supported content type when decoding messages, only the subtype - * is checked, e.g. */json, */xml. + * @param contentType the supported content type; only the subtype is checked when + * decoding, e.g. */json, */xml. If this contains a charset parameter, when + * encoding, the contentType header will not be set, when decoding, the raw bytes are + * passed to Jackson which can dynamically determine the encoding; otherwise the + * contentEncoding or default charset is used. * @param trustedPackages the trusted Java packages for deserialization * @see DefaultJackson2JavaTypeMapper#setTrustedPackages(String...) */ @@ -105,9 +115,50 @@ protected AbstractJackson2MessageConverter(ObjectMapper objectMapper, MimeType c Assert.notNull(contentType, "'contentType' must not be null"); this.objectMapper = objectMapper; this.supportedContentType = contentType; + this.supportedCTCharset = this.supportedContentType.getParameter("charset"); ((DefaultJackson2JavaTypeMapper) this.javaTypeMapper).setTrustedPackages(trustedPackages); } + + /** + * Get the supported content type; only the subtype is checked when decoding, e.g. + * */json, */xml. If this contains a charset parameter, when encoding, the + * contentType header will not be set, when decoding, the raw bytes are passed to + * Jackson which can dynamically determine the encoding; otherwise the contentEncoding + * or default charset is used. + * @return the supportedContentType + * @since 2.4.3 + */ + protected MimeType getSupportedContentType() { + return this.supportedContentType; + } + + + /** + * Set the supported content type; only the subtype is checked when decoding, e.g. + * */json, */xml. If this contains a charset parameter, when encoding, the + * contentType header will not be set, when decoding, the raw bytes are passed to + * Jackson which can dynamically determine the encoding; otherwise the contentEncoding + * or default charset is used. + * @param supportedContentType the supportedContentType to set. + * @since 2.4.3 + */ + public void setSupportedContentType(MimeType supportedContentType) { + Assert.notNull(supportedContentType, "'supportedContentType' cannot be null"); + this.supportedContentType = supportedContentType; + this.supportedCTCharset = this.supportedContentType.getParameter("charset"); + } + + /** + * When true, if jackson decodes the body as {@code null} convert to {@link Optional#empty()} + * instead of returning the original body. Default false. + * @param nullAsOptionalEmpty true to return empty. + * @since 2.4.7 + */ + public void setNullAsOptionalEmpty(boolean nullAsOptionalEmpty) { + this.nullAsOptionalEmpty = nullAsOptionalEmpty; + } + @Nullable public ClassMapper getClassMapper() { return this.classMapper; @@ -140,7 +191,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { } } - protected ClassLoader getClassLoader() { + protected @Nullable ClassLoader getClassLoader() { return this.classLoader; } @@ -149,7 +200,7 @@ public Jackson2JavaTypeMapper getJavaTypeMapper() { } /** - * Whether or not an explicit java type mapper has been provided. + * Whether an explicit java type mapper has been provided. * @return false if the default type mapper is being used. * @since 2.2 * @see #setJavaTypeMapper(Jackson2JavaTypeMapper) @@ -193,8 +244,8 @@ public void setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence typePreceden if (this.typeMapperSet) { throw new IllegalStateException("When providing your own type mapper, you should set the precedence on it"); } - if (this.javaTypeMapper instanceof DefaultJackson2JavaTypeMapper) { - ((DefaultJackson2JavaTypeMapper) this.javaTypeMapper).setTypePrecedence(typePrecedence); + if (this.javaTypeMapper instanceof DefaultJackson2JavaTypeMapper defaultJackson2JavaTypeMapper) { + defaultJackson2JavaTypeMapper.setTypePrecedence(typePrecedence); } else { throw new IllegalStateException("Type precedence is available with the DefaultJackson2JavaTypeMapper"); @@ -234,8 +285,8 @@ public void setUseProjectionForInterfaces(boolean useProjectionForInterfaces) { } /** - * By default the supported content type is assumed when there is no contentType - * property or it is set to the default ('application/octet-stream'). Set to 'false' + * By default, the supported content type is assumed when there is no contentType + * property, or it is set to the default ('application/octet-stream'). Set to 'false' * to revert to the previous behavior of returning an unconverted 'byte[]' when this * condition exists. * @param assumeSupportedContentType set false to not assume the content type is @@ -259,31 +310,49 @@ public Object fromMessage(Message message) throws MessageConversionException { public Object fromMessage(Message message, @Nullable Object conversionHint) throws MessageConversionException { Object content = null; MessageProperties properties = message.getMessageProperties(); - if (properties != null) { - String contentType = properties.getContentType(); - if ((this.assumeSupportedContentType // NOSONAR Boolean complexity - && (contentType == null || contentType.equals(MessageProperties.DEFAULT_CONTENT_TYPE))) - || (contentType != null && contentType.contains(this.supportedContentType.getSubtype()))) { - String encoding = properties.getContentEncoding(); - if (encoding == null) { - encoding = getDefaultCharset(); - } - content = doFromMessage(message, conversionHint, properties, encoding); - } - else { - if (this.log.isWarnEnabled()) { - this.log.warn("Could not convert incoming message with content-type [" - + contentType + "], '" + this.supportedContentType.getSubtype() + "' keyword missing."); - } + String contentType = properties.getContentType(); + // NOSONAR Boolean complexity + if (this.assumeSupportedContentType && contentType.equals(MessageProperties.DEFAULT_CONTENT_TYPE) + || contentType.contains(this.supportedContentType.getSubtype())) { + + String encoding = determineEncoding(properties, contentType); + content = doFromMessage(message, conversionHint, properties, encoding); + } + else { + if (this.log.isWarnEnabled()) { + this.log.warn("Could not convert incoming message with content-type [" + + contentType + "], '" + this.supportedContentType.getSubtype() + "' keyword missing."); } } if (content == null) { - content = message.getBody(); + if (this.nullAsOptionalEmpty) { + content = Optional.empty(); + } + else { + content = message.getBody(); + } } return content; } - private Object doFromMessage(Message message, Object conversionHint, MessageProperties properties, + private String determineEncoding(MessageProperties properties, @Nullable String contentType) { + String encoding = properties.getContentEncoding(); + if (encoding == null && contentType != null) { + try { + MimeType mimeType = MimeTypeUtils.parseMimeType(contentType); + encoding = mimeType.getParameter("charset"); + } + catch (RuntimeException e) { + // Ignore + } + } + if (encoding == null) { + encoding = this.supportedCTCharset != null ? this.supportedCTCharset : getDefaultCharset(); + } + return encoding; + } + + private Object doFromMessage(Message message, @Nullable Object conversionHint, MessageProperties properties, String encoding) { Object content = null; @@ -297,7 +366,8 @@ private Object doFromMessage(Message message, Object conversionHint, MessageProp return content; } - private Object convertContent(Message message, Object conversionHint, MessageProperties properties, String encoding) + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private Object convertContent(Message message, @Nullable Object conversionHint, MessageProperties properties, String encoding) throws IOException { Object content = null; @@ -305,15 +375,16 @@ private Object convertContent(Message message, Object conversionHint, MessagePro if (inferredType != null && this.useProjectionForInterfaces && inferredType.isInterface() && !inferredType.getRawClass().getPackage().getName().startsWith("java.util")) { // List etc content = this.projectingConverter.convert(message, inferredType.getRawClass()); + properties.setProjectionUsed(true); } else if (inferredType != null && this.alwaysConvertToInferredType) { - content = tryConverType(message, encoding, inferredType); + content = tryConvertType(message, encoding, inferredType); } if (content == null) { - if (conversionHint instanceof ParameterizedTypeReference) { + if (conversionHint instanceof ParameterizedTypeReference parameterizedTypeReference) { content = convertBytesToObject(message.getBody(), encoding, this.objectMapper.getTypeFactory().constructType( - ((ParameterizedTypeReference) conversionHint).getType())); + parameterizedTypeReference.getType())); } else if (getClassMapper() == null) { JavaType targetJavaType = getJavaTypeMapper() @@ -335,8 +406,7 @@ else if (getClassMapper() == null) { * Unfortunately, mapper.canDeserialize() always returns true (adds an AbstractDeserializer * to the cache); so all we can do is try a conversion. */ - @Nullable - private Object tryConverType(Message message, String encoding, JavaType inferredType) { + private @Nullable Object tryConvertType(Message message, String encoding, JavaType inferredType) { try { return convertBytesToObject(message.getBody(), encoding, inferredType); } @@ -347,11 +417,17 @@ private Object tryConverType(Message message, String encoding, JavaType inferred } private Object convertBytesToObject(byte[] body, String encoding, JavaType targetJavaType) throws IOException { + if (this.supportedCTCharset != null) { // Jackson will determine encoding + return this.objectMapper.readValue(body, targetJavaType); + } String contentAsString = new String(body, encoding); return this.objectMapper.readValue(contentAsString, targetJavaType); } private Object convertBytesToObject(byte[] body, String encoding, Class targetClass) throws IOException { + if (this.supportedCTCharset != null) { // Jackson will determine encoding + return this.objectMapper.readValue(body, this.objectMapper.constructType(targetClass)); + } String contentAsString = new String(body, encoding); return this.objectMapper.readValue(contentAsString, this.objectMapper.constructType(targetClass)); } @@ -369,20 +445,23 @@ protected Message createMessage(Object objectToConvert, MessageProperties messag byte[] bytes; try { - if (this.charsetIsUtf8) { + if (this.charsetIsUtf8 && this.supportedCTCharset == null) { bytes = this.objectMapper.writeValueAsBytes(objectToConvert); } else { String jsonString = this.objectMapper .writeValueAsString(objectToConvert); - bytes = jsonString.getBytes(getDefaultCharset()); + String encoding = this.supportedCTCharset != null ? this.supportedCTCharset : getDefaultCharset(); + bytes = jsonString.getBytes(encoding); } } catch (IOException e) { throw new MessageConversionException("Failed to convert Message content", e); } messageProperties.setContentType(this.supportedContentType.toString()); - messageProperties.setContentEncoding(getDefaultCharset()); + if (this.supportedCTCharset == null) { + messageProperties.setContentEncoding(getDefaultCharset()); + } messageProperties.setContentLength(bytes.length); if (getClassMapper() == null) { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java index 4745f9623d..ddb881af72 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,14 +20,14 @@ import java.util.HashMap; import java.util.Map; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.type.TypeFactory; - /** * Abstract type mapper. * @@ -35,6 +35,8 @@ * @author Sam Nelson * @author Andreas Asplund * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan */ public abstract class AbstractJavaTypeMapper implements BeanClassLoaderAware { @@ -44,11 +46,11 @@ public abstract class AbstractJavaTypeMapper implements BeanClassLoaderAware { public static final String DEFAULT_KEY_CLASSID_FIELD_NAME = "__KeyTypeId__"; - private final Map> idClassMapping = new HashMap>(); + private final Map> idClassMapping = new HashMap<>(); - private final Map, String> classIdMapping = new HashMap, String>(); + private final Map, String> classIdMapping = new HashMap<>(); - private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); public String getClassIdFieldName() { return DEFAULT_CLASSID_FIELD_NAME; @@ -72,7 +74,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } - protected ClassLoader getClassLoader() { + protected @Nullable ClassLoader getClassLoader() { return this.classLoader; } @@ -94,15 +96,12 @@ protected String retrieveHeader(MessageProperties properties, String headerName) return classId; } - @Nullable - protected String retrieveHeaderAsString(MessageProperties properties, String headerName) { - Map headers = properties.getHeaders(); + protected @Nullable String retrieveHeaderAsString(MessageProperties properties, String headerName) { + Map headers = properties.getHeaders(); Object classIdFieldNameValue = headers.get(headerName); - String classId = null; - if (classIdFieldNameValue != null) { - classId = classIdFieldNameValue.toString(); - } - return classId; + return classIdFieldNameValue != null + ? classIdFieldNameValue.toString() + : null; } private void createReverseMap() { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractMessageConverter.java index d12a74bba3..f3e6d2c77e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,10 @@ import java.lang.reflect.Type; import java.util.UUID; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; /** * Convenient base class for {@link MessageConverter} implementations. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverter.java index 14d6247ddd..4f5e6d24ae 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,12 +27,13 @@ * MessageConverters that potentially use Java deserialization. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.5.5 * */ public abstract class AllowedListDeserializingMessageConverter extends AbstractMessageConverter { - private final Set allowedListPatterns = new LinkedHashSet(); + private final Set allowedListPatterns = new LinkedHashSet<>(); /** * Set simple patterns for allowable packages/classes for deserialization. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java index 735d928ac5..0f6b9d10f3 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,11 +33,13 @@ * @author Eric Rizzo * @author Gary Russell * @author Artem Bilan + * @author Ngoc Nhan + * * @since 1.4.2 */ public class ContentTypeDelegatingMessageConverter implements MessageConverter { - private final Map delegates = new HashMap(); + private final Map delegates = new HashMap<>(); private final MessageConverter defaultConverter; @@ -50,7 +52,7 @@ public ContentTypeDelegatingMessageConverter() { } /** - * Constructs an instance using a the supplied default converter. + * Constructs an instance using the supplied default converter. * May be null meaning a strict content-type match is required. * @param defaultConverter the converter. */ @@ -105,12 +107,7 @@ protected MessageConverter getConverterForContentType(String contentType) { delegate = this.defaultConverter; } - if (delegate == null) { - throw new MessageConversionException("No delegate converter is specified for content type " + contentType); - } - else { - return delegate; - } + return delegate; } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultClassMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultClassMapper.java index 3f5c9cf3cb..104bf49fec 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultClassMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultClassMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,9 +25,10 @@ import java.util.Map.Entry; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -60,9 +61,9 @@ public class DefaultClassMapper implements ClassMapper, InitializingBean { private final Set trustedPackages = new LinkedHashSet<>(TRUSTED_PACKAGES); - private volatile Map> idClassMapping = new HashMap<>(); + private final Map, String> classIdMapping = new HashMap<>(); - private volatile Map, String> classIdMapping = new HashMap<>(); + private volatile Map> idClassMapping = new HashMap<>(); private volatile Class defaultMapClass = LinkedHashMap.class; // NOSONAR concrete type @@ -117,7 +118,7 @@ public void setIdClassMapping(Map> idClassMapping) { * @param trustedPackages the trusted Java packages for deserialization * @since 1.6.11 */ - public void setTrustedPackages(@Nullable String... trustedPackages) { + public void setTrustedPackages(String @Nullable ... trustedPackages) { if (trustedPackages != null) { for (String trusted : trustedPackages) { if ("*".equals(trusted)) { @@ -169,22 +170,14 @@ public void fromClass(Class clazz, MessageProperties properties) { @Override public Class toClass(MessageProperties properties) { - Map headers = properties.getHeaders(); + Map headers = properties.getHeaders(); Object classIdFieldNameValue = headers.get(getClassIdFieldName()); String classId = null; if (classIdFieldNameValue != null) { classId = classIdFieldNameValue.toString(); } if (classId == null) { - if (this.defaultType != null) { - return this.defaultType; - } - else { - throw new MessageConversionException( - "failed to convert Message content. Could not resolve " - + getClassIdFieldName() + " in header " + - "and no defaultType provided"); - } + return this.defaultType; } return toClass(classId); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java index a8aea4f702..c651c40c30 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,14 +21,14 @@ import java.util.List; import java.util.Set; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.type.TypeFactory; - /** * Jackson 2 type mapper. * @author Mark Pollack @@ -36,6 +36,7 @@ * @author Andreas Asplund * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan */ public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implements Jackson2JavaTypeMapper { @@ -45,15 +46,15 @@ public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implem "java.lang" ); - private final Set trustedPackages = new LinkedHashSet(TRUSTED_PACKAGES); + private final Set trustedPackages = new LinkedHashSet<>(TRUSTED_PACKAGES); private volatile TypePrecedence typePrecedence = TypePrecedence.INFERRED; /** * Return the precedence. * @return the precedence. - * @see #setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence) * @since 1.6. + * @see #setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence) */ @Override public TypePrecedence getTypePrecedence() { @@ -90,7 +91,7 @@ public void setTypePrecedence(TypePrecedence typePrecedence) { * @param trustedPackages the trusted Java packages for deserialization * @since 1.6.11 */ - public void setTrustedPackages(@Nullable String... trustedPackages) { + public void setTrustedPackages(String @Nullable ... trustedPackages) { if (trustedPackages != null) { for (String trusted : trustedPackages) { if ("*".equals(trusted)) { @@ -105,7 +106,7 @@ public void setTrustedPackages(@Nullable String... trustedPackages) { } @Override - public void addTrustedPackages(@Nullable String... packages) { + public void addTrustedPackages(String @Nullable ... packages) { setTrustedPackages(packages); } @@ -136,10 +137,7 @@ private boolean canConvert(JavaType inferredType) { if (inferredType.isContainerType() && inferredType.getContentType().isAbstract()) { return false; } - if (inferredType.getKeyType() != null && inferredType.getKeyType().isAbstract()) { - return false; - } - return true; + return inferredType.getKeyType() == null || !inferredType.getKeyType().isAbstract(); } private JavaType fromTypeHeader(MessageProperties properties, String typeIdHeader) { @@ -160,8 +158,7 @@ private JavaType fromTypeHeader(MessageProperties properties, String typeIdHeade } @Override - @Nullable - public JavaType getInferredType(MessageProperties properties) { + public @Nullable JavaType getInferredType(MessageProperties properties) { if (this.typePrecedence.equals(TypePrecedence.INFERRED) && hasInferredTypeHeader(properties)) { return fromInferredTypeHeader(properties); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java index aea13ae7ea..ab7ef8737b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,10 @@ package org.springframework.amqp.support.converter; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; - import com.fasterxml.jackson.databind.JavaType; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.MessageProperties; /** * Strategy for setting metadata on messages such that one can create the class that needs diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java index 9e1c3c721a..b69e1b5ade 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,12 @@ package org.springframework.amqp.support.converter; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.util.MimeTypeUtils; - import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.util.MimeTypeUtils; + /** * JSON converter that uses the Jackson 2 Json library. * @@ -41,6 +41,7 @@ public class Jackson2JsonMessageConverter extends AbstractJackson2MessageConvert * Construct with an internal {@link ObjectMapper} instance * and trusted packed to all ({@code *}). * @since 1.6.11 + * @see JacksonUtils#enhancedObjectMapper() */ public Jackson2JsonMessageConverter() { this("*"); @@ -53,10 +54,10 @@ public Jackson2JsonMessageConverter() { * @param trustedPackages the trusted Java packages for deserialization * @since 1.6.11 * @see DefaultJackson2JavaTypeMapper#setTrustedPackages(String...) + * @see JacksonUtils#enhancedObjectMapper() */ public Jackson2JsonMessageConverter(String... trustedPackages) { - this(new ObjectMapper(), trustedPackages); - this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + this(JacksonUtils.enhancedObjectMapper(), trustedPackages); } /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java index f544fdfa18..27b0c3379d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,12 @@ package org.springframework.amqp.support.converter; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.util.MimeTypeUtils; - import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.util.MimeTypeUtils; + /** * XML converter that uses the Jackson 2 Xml library. * diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java new file mode 100644 index 0000000000..f9c0348fff --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.support.converter; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + +import org.springframework.util.ClassUtils; + + +/** + * The utilities for Jackson {@link ObjectMapper} instances. + * + * @author Artem Bilan + * + * @since 3.1.1 + */ +public final class JacksonUtils { + + private static final boolean JDK8_MODULE_PRESENT = + ClassUtils.isPresent("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", null); + + private static final boolean PARAMETER_NAMES_MODULE_PRESENT = + ClassUtils.isPresent("com.fasterxml.jackson.module.paramnames.ParameterNamesModule", null); + + private static final boolean JAVA_TIME_MODULE_PRESENT = + ClassUtils.isPresent("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", null); + + private static final boolean JODA_MODULE_PRESENT = + ClassUtils.isPresent("com.fasterxml.jackson.datatype.joda.JodaModule", null); + + private static final boolean KOTLIN_MODULE_PRESENT = + ClassUtils.isPresent("kotlin.Unit", null) && + ClassUtils.isPresent("com.fasterxml.jackson.module.kotlin.KotlinModule", null); + + /** + * Factory for {@link ObjectMapper} instances with registered well-known modules + * and disabled {@link MapperFeature#DEFAULT_VIEW_INCLUSION} and + * {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} features. + * @return the {@link ObjectMapper} instance. + */ + public static ObjectMapper enhancedObjectMapper() { + ObjectMapper objectMapper = JsonMapper.builder() + .configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .build(); + registerWellKnownModulesIfAvailable(objectMapper); + return objectMapper; + } + + private static void registerWellKnownModulesIfAvailable(ObjectMapper objectMapper) { + if (JDK8_MODULE_PRESENT) { + objectMapper.registerModule(Jdk8ModuleProvider.MODULE); + } + + if (PARAMETER_NAMES_MODULE_PRESENT) { + objectMapper.registerModule(ParameterNamesProvider.MODULE); + } + + if (JAVA_TIME_MODULE_PRESENT) { + objectMapper.registerModule(JavaTimeModuleProvider.MODULE); + } + + if (JODA_MODULE_PRESENT) { + objectMapper.registerModule(JodaModuleProvider.MODULE); + } + + if (KOTLIN_MODULE_PRESENT) { + objectMapper.registerModule(KotlinModuleProvider.MODULE); + } + } + + private JacksonUtils() { + } + + private static final class Jdk8ModuleProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.datatype.jdk8.Jdk8Module(); + + } + + private static final class ParameterNamesProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.module.paramnames.ParameterNamesModule(); + + } + + private static final class JavaTimeModuleProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule(); + + } + + private static final class JodaModuleProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.datatype.joda.JodaModule(); + + } + + private static final class KotlinModuleProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.module.kotlin.KotlinModule.Builder().build(); + + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java index 25f77c9cd7..0c0129042d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,10 +23,11 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.oxm.Marshaller; import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.XmlMappingException; @@ -40,16 +41,19 @@ * @author Arjen Poutsma * @author Juergen Hoeller * @author James Carr + * @author Ngoc Nhan * @see org.springframework.amqp.core.AmqpTemplate#convertAndSend(Object) * @see org.springframework.amqp.core.AmqpTemplate#receiveAndConvert() */ public class MarshallingMessageConverter extends AbstractMessageConverter implements InitializingBean { + + @SuppressWarnings("NullAway.Init") private volatile Marshaller marshaller; + @SuppressWarnings("NullAway.Init") private volatile Unmarshaller unmarshaller; - private volatile String contentType; - + private volatile @Nullable String contentType; /** * Construct a new MarshallingMessageConverter with no {@link Marshaller} or {@link Unmarshaller} set. @@ -74,13 +78,12 @@ public MarshallingMessageConverter(Marshaller marshaller) { if (!(marshaller instanceof Unmarshaller)) { throw new IllegalArgumentException( "Marshaller [" + marshaller + "] does not implement the Unmarshaller " + - "interface. Please set an Unmarshaller explicitly by using the " + - "MarshallingMessageConverter(Marshaller, Unmarshaller) constructor."); - } - else { - this.marshaller = marshaller; - this.unmarshaller = (Unmarshaller) marshaller; + "interface. Please set an Unmarshaller explicitly by using the " + + "MarshallingMessageConverter(Marshaller, Unmarshaller) constructor."); } + + this.marshaller = marshaller; + this.unmarshaller = (Unmarshaller) marshaller; } /** @@ -96,7 +99,6 @@ public MarshallingMessageConverter(Marshaller marshaller, Unmarshaller unmarshal this.unmarshaller = unmarshaller; } - /** * Set the contentType to be used by this message converter. * @@ -132,7 +134,6 @@ public void afterPropertiesSet() { Assert.notNull(this.unmarshaller, "Property 'unmarshaller' is required"); } - /** * Marshals the given object to a {@link Message}. */ diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConversionException.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConversionException.java index 376a3c2429..c2e879394d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConversionException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConversionException.java @@ -23,7 +23,7 @@ * Exception to be thrown by message converters if they encounter a problem with converting a message or object. *

*

- * N.B. this is not an {@link AmqpException} because it is a a client exception, not a protocol or broker + * N.B. this is not an {@link AmqpException} because it is a client exception, not a protocol or broker * problem. *

* diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConverter.java index 77497e2718..fe857551b3 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,10 @@ import java.lang.reflect.Type; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; /** * Message converter interface. @@ -53,7 +54,7 @@ public interface MessageConverter { default Message toMessage(Object object, MessageProperties messageProperties, @Nullable Type genericType) throws MessageConversionException { - return toMessage(object, messageProperties); + return toMessage(object, messageProperties); } /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java index 0c91aac106..05a808b61c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author 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 @@ * is considered to be a request). * * @author Stephane Nicoll + * @author Ngoc Nhan * @since 1.4 */ public class MessagingMessageConverter implements MessageConverter, InitializingBean { @@ -104,11 +105,10 @@ public void afterPropertiesSet() { public org.springframework.amqp.core.Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { - if (!(object instanceof Message)) { + if (!(object instanceof Message input)) { throw new IllegalArgumentException("Could not convert [" + object + "] - only [" + Message.class.getName() + "] is handled by this converter"); } - Message input = (Message) object; this.headerMapper.fromHeaders(input.getHeaders(), messageProperties); org.springframework.amqp.core.Message amqpMessage = this.payloadConverter.toMessage( input.getPayload(), messageProperties); @@ -125,9 +125,6 @@ public org.springframework.amqp.core.Message toMessage(Object object, MessagePro @SuppressWarnings("unchecked") @Override public Object fromMessage(org.springframework.amqp.core.Message message) throws MessageConversionException { - if (message == null) { - return null; - } Map mappedHeaders = this.headerMapper.toHeaders(message.getMessageProperties()); Object convertedObject = extractPayload(message); if (convertedObject == null) { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java index 2b3b7b8d9d..b93901ad2c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author 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.ByteArrayInputStream; import java.lang.reflect.Type; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; + import org.springframework.amqp.core.Message; import org.springframework.core.ResolvableType; import org.springframework.data.projection.MethodInterceptorFactory; @@ -27,9 +30,6 @@ import org.springframework.data.web.JsonProjectingMethodInterceptorFactory; import org.springframework.util.Assert; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; - /** * Uses a Spring Data {@link ProjectionFactory} to bind incoming messages to projection * interfaces. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java index da0b8b8bfc..2bb40fa8ae 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 the original author 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,6 @@ import org.springframework.amqp.AmqpRemoteException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.remoting.support.RemoteInvocationResult; import org.springframework.util.Assert; /** @@ -32,28 +30,17 @@ * @since 2.0 * */ -public class RemoteInvocationAwareMessageConverterAdapter implements MessageConverter, BeanClassLoaderAware { +public class RemoteInvocationAwareMessageConverterAdapter implements MessageConverter { private final MessageConverter delegate; - private final boolean shouldSetClassLoader; - public RemoteInvocationAwareMessageConverterAdapter() { this.delegate = new SimpleMessageConverter(); - this.shouldSetClassLoader = true; } public RemoteInvocationAwareMessageConverterAdapter(MessageConverter delegate) { Assert.notNull(delegate, "'delegate' converter cannot be null"); this.delegate = delegate; - this.shouldSetClassLoader = false; - } - - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - if (this.shouldSetClassLoader) { - ((SimpleMessageConverter) this.delegate).setBeanClassLoader(classLoader); - } } @Override @@ -64,9 +51,9 @@ public Message toMessage(Object object, MessageProperties messageProperties) thr @Override public Object fromMessage(Message message) throws MessageConversionException { Object result = this.delegate.fromMessage(message); - if (result instanceof RemoteInvocationResult) { + if (result instanceof RemoteInvocationResult remoteInvocationResult) { try { - result = ((RemoteInvocationResult) result).recreate(); + result = remoteInvocationResult.recreate(); if (result == null) { throw new MessageConversionException("RemoteInvocationResult returned null"); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java new file mode 100644 index 0000000000..47d945ab3a --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java @@ -0,0 +1,155 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.support.converter; + +import java.io.Serial; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; + +import org.jspecify.annotations.Nullable; + +/** + * Encapsulates a remote invocation result, holding a result value or an exception. + * + * @author Juergen Hoeller + * @author Gary Russell + * @author Ngoc Nhan + * @since 3.0 + */ +public class RemoteInvocationResult implements Serializable { + + /** Use serialVersionUID from Spring 1.1 for interoperability. */ + @Serial + private static final long serialVersionUID = 2138555143707773549L; + + + private transient @Nullable Object value; + + private @Nullable Throwable exception; + + + /** + * Create a new RemoteInvocationResult for the given result value. + * @param value the result value returned by a successful invocation + * of the target method + */ + public RemoteInvocationResult(@Nullable Object value) { + this.value = value; + } + + /** + * Create a new RemoteInvocationResult for the given exception. + * @param exception the exception thrown by an unsuccessful invocation + * of the target method + */ + public RemoteInvocationResult(@Nullable Throwable exception) { + this.exception = exception; + } + + /** + * Create a new RemoteInvocationResult for JavaBean-style deserialization + * (e.g. with Jackson). + * @see #setValue + * @see #setException + */ + public RemoteInvocationResult() { + } + + + /** + * Set the result value returned by a successful invocation of the + * target method, if any. + *

This setter is intended for JavaBean-style deserialization. + * Use {@link #RemoteInvocationResult(Object)} otherwise. + * @see #RemoteInvocationResult() + */ + public void setValue(@Nullable Object value) { + this.value = value; + } + + /** + * Return the result value returned by a successful invocation + * of the target method, if any. + * @see #hasException + */ + @Nullable + public Object getValue() { + return this.value; + } + + /** + * Set the exception thrown by an unsuccessful invocation of the + * target method, if any. + *

This setter is intended for JavaBean-style deserialization. + * Use {@link #RemoteInvocationResult(Throwable)} otherwise. + * @see #RemoteInvocationResult() + */ + public void setException(@Nullable Throwable exception) { + this.exception = exception; + } + + /** + * Return the exception thrown by an unsuccessful invocation + * of the target method, if any. + * @see #hasException + */ + @Nullable + public Throwable getException() { + return this.exception; + } + + /** + * Return whether this invocation result holds an exception. + * If this returns {@code false}, the result value applies + * (even if it is {@code null}). + * @see #getValue + * @see #getException + */ + public boolean hasException() { + return (this.exception != null); + } + + /** + * Return whether this invocation result holds an InvocationTargetException, + * thrown by an invocation of the target method itself. + * @see #hasException() + */ + public boolean hasInvocationTargetException() { + return (this.exception instanceof InvocationTargetException); + } + + + /** + * Recreate the invocation result, either returning the result value + * in case of a successful invocation of the target method, or + * rethrowing the exception thrown by the target method. + * @return the result value, if any + * @throws Throwable the exception, if any + */ + @Nullable + public Object recreate() throws Throwable { + if (this.exception != null) { + Throwable exToThrow = this.exception instanceof InvocationTargetException invocationTargetException + ? invocationTargetException.getTargetException() + : this.exception; + RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); + throw exToThrow; + } + return this.value; + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java new file mode 100644 index 0000000000..f8a8d8e385 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.support.converter; + +import java.util.HashSet; +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +/** + * General utilities for handling remote invocations. + * + *

Mainly intended for use within the remoting framework. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class RemoteInvocationUtils { + + /** + * Fill the current client-side stack trace into the given exception. + *

The given exception is typically thrown on the server and serialized + * as-is, with the client wanting it to contain the client-side portion + * of the stack trace as well. What we can do here is to update the + * {@code StackTraceElement} array with the current client-side stack + * trace, provided that we run on JDK 1.4+. + * @param ex the exception to update + * @see Throwable#getStackTrace() + * @see Throwable#setStackTrace(StackTraceElement[]) + */ + public static void fillInClientStackTraceIfPossible(@Nullable Throwable ex) { + if (ex != null) { + StackTraceElement[] clientStack = new Throwable().getStackTrace(); + Set visitedExceptions = new HashSet<>(); + Throwable exToUpdate = ex; + while (exToUpdate != null && !visitedExceptions.contains(exToUpdate)) { + StackTraceElement[] serverStack = exToUpdate.getStackTrace(); + StackTraceElement[] combinedStack = new StackTraceElement[serverStack.length + clientStack.length]; + System.arraycopy(serverStack, 0, combinedStack, 0, serverStack.length); + System.arraycopy(clientStack, 0, combinedStack, serverStack.length, clientStack.length); + exToUpdate.setStackTrace(combinedStack); + visitedExceptions.add(exToUpdate); + exToUpdate = exToUpdate.getCause(); + } + } + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java index 5b12f288b6..4bd99684d4 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,16 +22,19 @@ import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.ConfigurableObjectInputStream; -import org.springframework.core.NestedIOException; import org.springframework.core.serializer.DefaultDeserializer; import org.springframework.core.serializer.DefaultSerializer; import org.springframework.core.serializer.Deserializer; import org.springframework.core.serializer.Serializer; +import org.springframework.util.ClassUtils; /** * Implementation of {@link MessageConverter} that can work with Strings or native objects @@ -47,27 +50,28 @@ * * @author Dave Syer * @author Gary Russell + * @author Artem Bilan */ -public class SerializerMessageConverter extends AllowedListDeserializingMessageConverter { +public class SerializerMessageConverter extends AllowedListDeserializingMessageConverter + implements BeanClassLoaderAware { - public static final String DEFAULT_CHARSET = "UTF-8"; + public static final String DEFAULT_CHARSET = StandardCharsets.UTF_8.name(); - private volatile String defaultCharset = DEFAULT_CHARSET; + private String defaultCharset = DEFAULT_CHARSET; - private volatile Serializer serializer = new DefaultSerializer(); + private Serializer serializer = new DefaultSerializer(); - private volatile Deserializer deserializer = new DefaultDeserializer(); + private Deserializer deserializer = new DefaultDeserializer(); - private volatile boolean ignoreContentType = false; + private boolean ignoreContentType = false; - private volatile ClassLoader defaultDeserializerClassLoader; + private @Nullable ClassLoader defaultDeserializerClassLoader = ClassUtils.getDefaultClassLoader(); - private volatile boolean usingDefaultDeserializer = true; + private boolean usingDefaultDeserializer = true; /** * Flag to signal that the content type should be ignored and the deserializer used irrespective if it is a text * message. Defaults to false, in which case the default encoding is used to convert a text message to a String. - * * @param ignoreContentType the flag value to set */ public void setIgnoreContentType(boolean ignoreContentType) { @@ -77,16 +81,14 @@ public void setIgnoreContentType(boolean ignoreContentType) { /** * Specify the default charset to use when converting to or from text-based Message body content. If not specified, * the charset will be "UTF-8". - * * @param defaultCharset The default charset. */ - public void setDefaultCharset(String defaultCharset) { + public void setDefaultCharset(@Nullable String defaultCharset) { this.defaultCharset = (defaultCharset != null) ? defaultCharset : DEFAULT_CHARSET; } /** * The serializer to use for converting Java objects to message bodies. - * * @param serializer the serializer to set */ public void setSerializer(Serializer serializer) { @@ -95,24 +97,16 @@ public void setSerializer(Serializer serializer) { /** * The deserializer to use for converting from message body to Java object. - * * @param deserializer the deserializer to set */ public void setDeserializer(Deserializer deserializer) { this.deserializer = deserializer; - if (this.deserializer.getClass().equals(DefaultDeserializer.class)) { - try { - this.defaultDeserializerClassLoader = (ClassLoader) new DirectFieldAccessor(deserializer) - .getPropertyValue("classLoader"); - } - catch (Exception e) { - // no-op - } - this.usingDefaultDeserializer = true; - } - else { - this.usingDefaultDeserializer = false; - } + this.usingDefaultDeserializer = false; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.defaultDeserializerClassLoader = classLoader; } /** @@ -122,15 +116,12 @@ public void setDeserializer(Deserializer deserializer) { public Object fromMessage(Message message) throws MessageConversionException { Object content = null; MessageProperties properties = message.getMessageProperties(); - if (properties != null) { - String contentType = properties.getContentType(); - if (contentType != null && contentType.startsWith("text") && !this.ignoreContentType) { - content = asString(message, properties); - } - else if (contentType != null && contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT) - || this.ignoreContentType) { - content = deserialize(message); - } + String contentType = properties.getContentType(); + if (contentType.startsWith("text") && !this.ignoreContentType) { + content = asString(message, properties); + } + else if (contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT) || this.ignoreContentType) { + content = deserialize(message); } if (content == null) { content = message.getBody(); @@ -168,21 +159,21 @@ private Object asString(Message message, MessageProperties properties) { private Object deserialize(ByteArrayInputStream inputStream) throws IOException { try (ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, - this.defaultDeserializerClassLoader) { - - @Override - protected Class resolveClass(ObjectStreamClass classDesc) - throws IOException, ClassNotFoundException { - Class clazz = super.resolveClass(classDesc); - checkAllowedList(clazz); - return clazz; - } + this.defaultDeserializerClassLoader) { + + @Override + protected Class resolveClass(ObjectStreamClass classDesc) + throws IOException, ClassNotFoundException { + Class clazz = super.resolveClass(classDesc); + checkAllowedList(clazz); + return clazz; + } - }) { + }) { return objectInputStream.readObject(); } catch (ClassNotFoundException ex) { - throw new NestedIOException("Failed to deserialize object type", ex); + throw new IOException("Failed to deserialize object type", ex); } } @@ -192,10 +183,11 @@ protected Class resolveClass(ObjectStreamClass classDesc) @Override protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + byte[] bytes; - if (object instanceof String) { + if (object instanceof String string) { try { - bytes = ((String) object).getBytes(this.defaultCharset); + bytes = string.getBytes(this.defaultCharset); } catch (UnsupportedEncodingException e) { throw new MessageConversionException("failed to convert Message content", e); @@ -203,8 +195,8 @@ protected Message createMessage(Object object, MessageProperties messageProperti messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); messageProperties.setContentEncoding(this.defaultCharset); } - else if (object instanceof byte[]) { - bytes = (byte[]) object; + else if (object instanceof byte[] objectBytes) { + bytes = objectBytes; messageProperties.setContentType(MessageProperties.CONTENT_TYPE_BYTES); } else { @@ -218,9 +210,8 @@ else if (object instanceof byte[]) { bytes = output.toByteArray(); messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT); } - if (bytes != null) { - messageProperties.setContentLength(bytes.length); - } + + messageProperties.setContentLength(bytes.length); return new Message(bytes, messageProperties); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java index 732c339481..f009bd85a4 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,13 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.utils.SerializationUtils; import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.core.ConfigurableObjectInputStream; import org.springframework.util.ClassUtils; /** @@ -40,51 +43,30 @@ * @author Mark Fisher * @author Oleg Zhurakousky * @author Gary Russell + * @author Artem Bilan */ public class SimpleMessageConverter extends AllowedListDeserializingMessageConverter implements BeanClassLoaderAware { public static final String DEFAULT_CHARSET = "UTF-8"; - private volatile String defaultCharset = DEFAULT_CHARSET; - - private String codebaseUrl; - - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - - @Override - public void setBeanClassLoader(ClassLoader beanClassLoader) { - this.beanClassLoader = beanClassLoader; - } + private String defaultCharset = DEFAULT_CHARSET; - /** - * Set the codebase URL to download classes from if not found locally. Can consist of - * multiple URLs, separated by spaces. - *

- * Follows RMI's codebase conventions for dynamic class download. - * - * @param codebaseUrl The codebase URL. - * - * @deprecated due to deprecation of - * {@link org.springframework.remoting.rmi.CodebaseAwareObjectInputStream}. - * - * @see org.springframework.remoting.rmi.CodebaseAwareObjectInputStream - * @see java.rmi.server.RMIClassLoader - */ - @Deprecated - public void setCodebaseUrl(String codebaseUrl) { - this.codebaseUrl = codebaseUrl; - } + private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); /** * Specify the default charset to use when converting to or from text-based * Message body content. If not specified, the charset will be "UTF-8". - * * @param defaultCharset The default charset. */ - public void setDefaultCharset(String defaultCharset) { + public void setDefaultCharset(@Nullable String defaultCharset) { this.defaultCharset = (defaultCharset != null) ? defaultCharset : DEFAULT_CHARSET; } + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + /** * Converts from a AMQP Message to an Object. */ @@ -92,31 +74,26 @@ public void setDefaultCharset(String defaultCharset) { public Object fromMessage(Message message) throws MessageConversionException { Object content = null; MessageProperties properties = message.getMessageProperties(); - if (properties != null) { - String contentType = properties.getContentType(); - if (contentType != null && contentType.startsWith("text")) { - String encoding = properties.getContentEncoding(); - if (encoding == null) { - encoding = this.defaultCharset; - } - try { - content = new String(message.getBody(), encoding); - } - catch (UnsupportedEncodingException e) { - throw new MessageConversionException( - "failed to convert text-based Message content", e); - } + String contentType = properties.getContentType(); + if (contentType.startsWith("text")) { + String encoding = properties.getContentEncoding(); + if (encoding == null) { + encoding = this.defaultCharset; + } + try { + content = new String(message.getBody(), encoding); + } + catch (UnsupportedEncodingException e) { + throw new MessageConversionException("failed to convert text-based Message content", e); } - else if (contentType != null && - contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)) { - try { - content = SerializationUtils.deserialize( - createObjectInputStream(new ByteArrayInputStream(message.getBody()), this.codebaseUrl)); - } - catch (IOException | IllegalArgumentException | IllegalStateException e) { - throw new MessageConversionException( - "failed to convert serialized Message content", e); - } + } + else if (contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)) { + try { + content = SerializationUtils.deserialize( + createObjectInputStream(new ByteArrayInputStream(message.getBody()))); + } + catch (IOException | IllegalArgumentException | IllegalStateException e) { + throw new MessageConversionException("failed to convert serialized Message content", e); } } if (content == null) { @@ -129,19 +106,20 @@ else if (contentType != null && * Creates an AMQP Message from the provided Object. */ @Override - protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + protected Message createMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException { + byte[] bytes = null; - if (object instanceof byte[]) { - bytes = (byte[]) object; + if (object instanceof byte[] objectBytes) { + bytes = objectBytes; messageProperties.setContentType(MessageProperties.CONTENT_TYPE_BYTES); } - else if (object instanceof String) { + else if (object instanceof String string) { try { - bytes = ((String) object).getBytes(this.defaultCharset); + bytes = string.getBytes(this.defaultCharset); } catch (UnsupportedEncodingException e) { - throw new MessageConversionException( - "failed to convert to Message content", e); + throw new MessageConversionException("failed to convert to Message content", e); } messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); messageProperties.setContentEncoding(this.defaultCharset); @@ -151,8 +129,7 @@ else if (object instanceof Serializable) { bytes = SerializationUtils.serialize(object); } catch (IllegalArgumentException e) { - throw new MessageConversionException( - "failed to convert to serialized Message content", e); + throw new MessageConversionException("failed to convert to serialized Message content", e); } messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT); } @@ -165,18 +142,15 @@ else if (object instanceof Serializable) { } /** - * Create an ObjectInputStream for the given InputStream and codebase. The default implementation creates a - * CodebaseAwareObjectInputStream. + * Create an ObjectInputStream for the given InputStream. The default + * implementation creates an {@link ConfigurableObjectInputStream} against configured {@link ClassLoader}. + * The class for object to deserialize is checked against {@code allowedListPatterns}. * @param is the InputStream to read from - * @param codebaseUrl the codebase URL to load classes from if not found locally (can be null) * @return the new ObjectInputStream instance to use * @throws IOException if creation of the ObjectInputStream failed - * @see org.springframework.remoting.rmi.CodebaseAwareObjectInputStream */ - @SuppressWarnings("deprecation") - protected ObjectInputStream createObjectInputStream(InputStream is, String codebaseUrl) throws IOException { - return new org.springframework.remoting.rmi.CodebaseAwareObjectInputStream(is, this.beanClassLoader, - codebaseUrl) { + protected ObjectInputStream createObjectInputStream(InputStream is) throws IOException { + return new ConfigurableObjectInputStream(is, this.classLoader) { @Override protected Class resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SmartMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SmartMessageConverter.java index d3aca01c51..50a11ec9d7 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SmartMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SmartMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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.amqp.support.converter; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; /** @@ -41,6 +43,6 @@ public interface SmartMessageConverter extends MessageConverter { * @throws MessageConversionException if the conversion fails. * @see #fromMessage(Message) */ - Object fromMessage(Message message, Object conversionHint) throws MessageConversionException; + Object fromMessage(Message message, @Nullable Object conversionHint) throws MessageConversionException; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/package-info.java index ee1b21b087..47e4671c0d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for supporting message conversion. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.support.converter; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/support/package-info.java index 997e2eb056..688dd25f14 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/package-info.java @@ -1,4 +1,5 @@ /** * Package for Spring AMQP support classes. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.support; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractCompressingPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractCompressingPostProcessor.java index 006975db2e..42b637df31 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractCompressingPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractCompressingPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,8 +80,8 @@ public AbstractCompressingPostProcessor(boolean autoDecompress) { /** * Flag to indicate if {@link MessageProperties} should be used as is or cloned for new message * after compression. - * By default this flag is turned off for better performance since in most cases the original message - * is not used any more. + * By default, this flag is turned off for better performance since in most cases the original message + * is not used anymore. * @param copyProperties clone or reuse original message properties. * @since 2.1.5 */ diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java index a8274c11ae..5d56d217f3 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author 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,9 @@ * the final content encoding of the decompressed message. * * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan + * * @since 1.4.2 */ public abstract class AbstractDecompressingPostProcessor implements MessagePostProcessor, Ordered { @@ -83,14 +86,16 @@ protected void setOrder(int order) { public Message postProcessMessage(Message message) throws AmqpException { Object autoDecompress = message.getMessageProperties().getHeaders() .get(MessageProperties.SPRING_AUTO_DECOMPRESS); - if (this.alwaysDecompress || (autoDecompress instanceof Boolean && ((Boolean) autoDecompress))) { + if (this.alwaysDecompress || (autoDecompress instanceof Boolean isAutoDecompress && isAutoDecompress)) { ByteArrayInputStream zipped = new ByteArrayInputStream(message.getBody()); try { InputStream unzipper = getDecompressorStream(zipped); ByteArrayOutputStream out = new ByteArrayOutputStream(); FileCopyUtils.copy(unzipper, out); MessageProperties messageProperties = message.getMessageProperties(); - String encoding = messageProperties.getContentEncoding(); + String contentEncoding = messageProperties.getContentEncoding(); + Assert.hasText(contentEncoding, "The 'encoding' message property is required"); + String encoding = contentEncoding; int delimAt = encoding.indexOf(':'); if (delimAt < 0) { delimAt = encoding.indexOf(','); @@ -104,9 +109,7 @@ public Message postProcessMessage(Message message) throws AmqpException { messageProperties.setContentEncoding(null); } else { - messageProperties.setContentEncoding(messageProperties.getContentEncoding() - .substring(delimAt + 1) - .trim()); + messageProperties.setContentEncoding(contentEncoding.substring(delimAt + 1).trim()); } messageProperties.getHeaders().remove(MessageProperties.SPRING_AUTO_DECOMPRESS); return new Message(out.toByteArray(), messageProperties); @@ -115,9 +118,8 @@ public Message postProcessMessage(Message message) throws AmqpException { throw new AmqpIOException(e); } } - else { - return message; - } + + return message; } /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DeflaterPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DeflaterPostProcessor.java index 90ae7032ce..92cde7a474 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DeflaterPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DeflaterPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author or 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.amqp.support.postprocessor; -import java.io.IOException; import java.io.OutputStream; import java.util.zip.DeflaterOutputStream; @@ -38,7 +37,7 @@ public DeflaterPostProcessor(boolean autoDecompress) { } @Override - protected OutputStream getCompressorStream(OutputStream zipped) throws IOException { + protected OutputStream getCompressorStream(OutputStream zipped) { return new DeflaterPostProcessor.SettableLevelDeflaterOutputStream(zipped, getLevel()); } @@ -55,4 +54,5 @@ private static final class SettableLevelDeflaterOutputStream extends DeflaterOut } } + } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java index e3e247f943..0651734b8c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2024 the original author 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,12 @@ * * @author Gary Russell * @author David Diehl + * @author Ngoc Nhan * @since 1.4.2 */ public class DelegatingDecompressingPostProcessor implements MessagePostProcessor, Ordered { - private final Map decompressors = new HashMap(); + private final Map decompressors = new HashMap<>(); private int order; @@ -97,22 +98,20 @@ public Message postProcessMessage(Message message) throws AmqpException { if (encoding == null) { return message; } - else { - int delimAt = encoding.indexOf(':'); - if (delimAt < 0) { - delimAt = encoding.indexOf(','); - } - if (delimAt > 0) { - encoding = encoding.substring(0, delimAt); - } - MessagePostProcessor decompressor = this.decompressors.get(encoding); - if (decompressor != null) { - return decompressor.postProcessMessage(message); - } - else { - return message; - } + + int delimAt = encoding.indexOf(':'); + if (delimAt < 0) { + delimAt = encoding.indexOf(','); + } + if (delimAt > 0) { + encoding = encoding.substring(0, delimAt); } + MessagePostProcessor decompressor = this.decompressors.get(encoding); + if (decompressor != null) { + return decompressor.postProcessMessage(message); + } + + return message; } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/InflaterPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/InflaterPostProcessor.java index 0d146642e7..c515dea410 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/InflaterPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/InflaterPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author or 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.amqp.support.postprocessor; -import java.io.IOException; import java.io.InputStream; import java.util.zip.InflaterInputStream; @@ -28,6 +27,7 @@ * @since 2.2 */ public class InflaterPostProcessor extends AbstractDecompressingPostProcessor { + public InflaterPostProcessor() { } @@ -35,11 +35,12 @@ public InflaterPostProcessor(boolean alwaysDecompress) { super(alwaysDecompress); } - protected InputStream getDecompressorStream(InputStream zipped) throws IOException { + protected InputStream getDecompressorStream(InputStream zipped) { return new InflaterInputStream(zipped); } protected String getEncoding() { return "deflate"; } + } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java index bf2ed742d8..e69d83e444 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author 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,18 @@ * Utilities for message post processors. * * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan + * * @since 1.4.2 * */ public final class MessagePostProcessorUtils { public static Collection sort(Collection processors) { - List priorityOrdered = new ArrayList(); - List ordered = new ArrayList(); - List unOrdered = new ArrayList(); + List priorityOrdered = new ArrayList<>(); + List ordered = new ArrayList<>(); + List unOrdered = new ArrayList<>(); for (MessagePostProcessor processor : processors) { if (processor instanceof PriorityOrdered) { priorityOrdered.add(processor); @@ -49,9 +52,8 @@ else if (processor instanceof Ordered) { unOrdered.add(processor); } } - List sorted = new ArrayList(); OrderComparator.sort(priorityOrdered); - sorted.addAll(priorityOrdered); + List sorted = new ArrayList<>(priorityOrdered); OrderComparator.sort(ordered); sorted.addAll(ordered); sorted.addAll(unOrdered); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/package-info.java index 6644991861..e70ca88ce6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/package-info.java @@ -1,4 +1,5 @@ /** * Package for Spring AMQP message post processors. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.support.postprocessor; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java index e628e441b0..1a1a31af2f 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -26,6 +28,8 @@ * the singleton {@link #INSTANCE} and then chain calls to the utility methods. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.1.4 * */ @@ -40,15 +44,15 @@ private JavaUtils() { } /** - * Invoke {@link Consumer#accept(Object)} with the value if the condition is true. + * Invoke {@link Consumer#accept(Object)} with the value if it is not null and the condition is true. * @param condition the condition. - * @param value the value. + * @param value the value. Skipped if null. * @param consumer the consumer. * @param the value type. * @return this. */ - public JavaUtils acceptIfCondition(boolean condition, T value, Consumer consumer) { - if (condition) { + public JavaUtils acceptIfCondition(boolean condition, @Nullable T value, Consumer consumer) { + if (condition && value != null) { consumer.accept(value); } return this; @@ -61,7 +65,7 @@ public JavaUtils acceptIfCondition(boolean condition, T value, Consumer c * @param the value type. * @return this. */ - public JavaUtils acceptIfNotNull(T value, Consumer consumer) { + public JavaUtils acceptIfNotNull(@Nullable T value, Consumer consumer) { if (value != null) { consumer.accept(value); } @@ -74,7 +78,7 @@ public JavaUtils acceptIfNotNull(T value, Consumer consumer) { * @param consumer the consumer. * @return this. */ - public JavaUtils acceptIfHasText(String value, Consumer consumer) { + public JavaUtils acceptIfHasText(@Nullable String value, Consumer consumer) { if (StringUtils.hasText(value)) { consumer.accept(value); } @@ -109,7 +113,7 @@ public JavaUtils acceptIfCondition(boolean condition, T1 t1, T2 t2, BiC * @param the second argument type. * @return this. */ - public JavaUtils acceptIfNotNull(T1 t1, T2 t2, BiConsumer consumer) { + public JavaUtils acceptIfNotNull(T1 t1, @Nullable T2 t2, BiConsumer consumer) { if (t2 != null) { consumer.accept(t1, t2); } @@ -125,11 +129,31 @@ public JavaUtils acceptIfNotNull(T1 t1, T2 t2, BiConsumer consu * @param consumer the consumer. * @return this. */ - public JavaUtils acceptIfHasText(T t1, String value, BiConsumer consumer) { + public JavaUtils acceptIfHasText(T t1, @Nullable String value, BiConsumer consumer) { if (StringUtils.hasText(value)) { consumer.accept(t1, value); } return this; } + + /** + * Invoke {@link Consumer#accept(Object)} with the value or alternative if one of them is not null. + * @param value the value. + * @param alternative the other value if the {@code value} argument is null. + * @param consumer the consumer. + * @param the value type. + * @return this. + * @since 4.0 + */ + public JavaUtils acceptOrElseIfNotNull(@Nullable T value, @Nullable T alternative, Consumer consumer) { + if (value != null) { + consumer.accept(value); + } + else if (alternative != null) { + consumer.accept(alternative); + } + return this; + } + } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/MapBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/MapBuilder.java index 1fea0e2d65..e3afc18117 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/MapBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/MapBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 the original author 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,11 +26,12 @@ * @param the value type. * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan * @since 2.0 */ public class MapBuilder, K, V> { - private final Map map = new HashMap(); + private final Map map = new HashMap<>(); public B put(K key, V value) { this.map.put(key, value); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java index ccdb9ce6b3..131a10dfd1 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2019 the original author or authors. + * Copyright 2006-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,8 +25,9 @@ import java.io.ObjectStreamClass; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ConfigurableObjectInputStream; -import org.springframework.core.NestedIOException; import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; @@ -35,9 +36,21 @@ * * @author Dave Syer * @author Gary Russell + * @author Artem Bilan */ public final class SerializationUtils { + private static final String TRUST_ALL_ENV = "SPRING_AMQP_DESERIALIZATION_TRUST_ALL"; + + private static final String TRUST_ALL_PROP = "spring.amqp.deserialization.trust.all"; + + private static final boolean TRUST_ALL; + + static { + TRUST_ALL = Boolean.parseBoolean(System.getenv(TRUST_ALL_ENV)) + || Boolean.parseBoolean(System.getProperty(TRUST_ALL_PROP)); + } + private SerializationUtils() { } @@ -47,7 +60,7 @@ private SerializationUtils() { * @param object the object to serialize * @return an array of bytes representing the object in a portable fashion */ - public static byte[] serialize(Object object) { + public static byte @Nullable [] serialize(@Nullable Object object) { if (object == null) { return null; } @@ -66,7 +79,7 @@ public static byte[] serialize(Object object) { * @param bytes a serialized object created * @return the result of deserializing the bytes */ - public static Object deserialize(byte[] bytes) { + public static @Nullable Object deserialize(byte @Nullable [] bytes) { if (bytes == null) { return null; } @@ -83,7 +96,7 @@ public static Object deserialize(byte[] bytes) { * @param stream an object stream created from a serialized object * @return the result of deserializing the bytes */ - public static Object deserialize(ObjectInputStream stream) { + public static @Nullable Object deserialize(@Nullable ObjectInputStream stream) { if (stream == null) { return null; } @@ -111,22 +124,22 @@ public static Object deserialize(InputStream inputStream, Set allowedLis throws IOException { try ( - ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, classLoader) { + ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, classLoader) { - @Override - protected Class resolveClass(ObjectStreamClass classDesc) - throws IOException, ClassNotFoundException { - Class clazz = super.resolveClass(classDesc); - checkAllowedList(clazz, allowedListPatterns); - return clazz; - } + @Override + protected Class resolveClass(ObjectStreamClass classDesc) + throws IOException, ClassNotFoundException { + Class clazz = super.resolveClass(classDesc); + checkAllowedList(clazz, allowedListPatterns); + return clazz; + } - }) { + }) { return objectInputStream.readObject(); } catch (ClassNotFoundException ex) { - throw new NestedIOException("Failed to deserialize object type", ex); + throw new IOException("Failed to deserialize object type", ex); } } @@ -137,11 +150,11 @@ protected Class resolveClass(ObjectStreamClass classDesc) * @since 2.1 */ public static void checkAllowedList(Class clazz, Set patterns) { - if (ObjectUtils.isEmpty(patterns)) { + if (TRUST_ALL && ObjectUtils.isEmpty(patterns)) { return; } - if (clazz.isArray() || clazz.isPrimitive() || clazz.equals(String.class) - || Number.class.isAssignableFrom(clazz)) { + if (clazz.isArray() || clazz.isPrimitive() || Number.class.isAssignableFrom(clazz) + || String.class.equals(clazz)) { return; } String className = clazz.getName(); @@ -150,7 +163,10 @@ public static void checkAllowedList(Class clazz, Set patterns) { return; } } - throw new SecurityException("Attempt to deserialize unauthorized " + clazz); + throw new SecurityException("Attempt to deserialize unauthorized " + clazz + + "; add allowed class name patterns to the message converter or, if you trust the message originator, " + + "set environment variable '" + + TRUST_ALL_ENV + "' or system property '" + TRUST_ALL_PROP + "' to true"); } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/package-info.java index 2a4a2ef163..08af0bf05b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/package-info.java @@ -1,4 +1,5 @@ /** * Provides utility classes to support Spring AMQP. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.utils; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java index 5934aa6a6a..a79991cecb 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2025 the original author or 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.amqp.utils.test; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.DirectFieldAccessor; import org.springframework.util.Assert; @@ -25,6 +27,7 @@ * @author Iwein Fuld * @author Oleg Zhurakousky * @author Gary Russell + * @author Ngoc Nhan * @since 1.2 */ public final class TestUtils { @@ -39,7 +42,7 @@ private TestUtils() { * @param propertyPath The path. * @return The field. */ - public static Object getPropertyValue(Object root, String propertyPath) { + public static @Nullable Object getPropertyValue(Object root, String propertyPath) { Object value = null; DirectFieldAccessor accessor = new DirectFieldAccessor(root); String[] tokens = propertyPath.split("\\."); @@ -47,19 +50,20 @@ public static Object getPropertyValue(Object root, String propertyPath) { value = accessor.getPropertyValue(tokens[i]); if (value != null) { accessor = new DirectFieldAccessor(value); + continue; } - else if (i == tokens.length - 1) { + + if (i == tokens.length - 1) { return null; } - else { - throw new IllegalArgumentException("intermediate property '" + tokens[i] + "' is null"); - } + + throw new IllegalArgumentException("intermediate property '" + tokens[i] + "' is null"); } return value; } @SuppressWarnings("unchecked") - public static T getPropertyValue(Object root, String propertyPath, Class type) { + public static @Nullable T getPropertyValue(Object root, String propertyPath, Class type) { Object value = getPropertyValue(root, propertyPath); if (value != null) { Assert.isAssignable(type, value.getClass()); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/test/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/package-info.java new file mode 100644 index 0000000000..9b28026a84 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides general testing utility classes. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.utils.test; diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java index feaea573b2..3bb17bf155 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,16 @@ package org.springframework.amqp.core; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Mark Pollack * @author Mark Fisher * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan */ public class AddressTests { @@ -100,6 +101,9 @@ public void testDirectReplyTo() { @Test public void testEquals() { assertThat(new Address("foo/bar")).isEqualTo(new Address("foo/bar")); + assertThat(new Address("foo", null)).isEqualTo(new Address("foo", null)); + assertThat(new Address(null, "bar")).isEqualTo(new Address(null, "bar")); + assertThat(new Address(null, null)).isEqualTo(new Address(null, null)); } } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderTests.java index d8aa174288..e11b3b5adb 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package org.springframework.amqp.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collections; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Mark Fisher * @author Artem Yakshin diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java new file mode 100644 index 0000000000..8c13b96a85 --- /dev/null +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.core; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Copy of {@link BindingBuilderTests} but using a queue with a lazy name. + * + * @author Mark Fisher + * @author Artem Yakshin + * @author Gary Russell + */ +public class BindingBuilderWithLazyQueueNameTests { + + private static Queue queue; + + @BeforeAll + public static void setUp() { + queue = new Queue(""); + queue.setActualName("actual"); + } + + @Test + public void fanoutBinding() { + FanoutExchange fanoutExchange = new FanoutExchange("f"); + Binding binding = BindingBuilder.bind(queue).to(fanoutExchange); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(fanoutExchange.getName()); + assertThat(binding.getRoutingKey()).isEqualTo(""); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + } + + @Test + public void directBinding() { + DirectExchange directExchange = new DirectExchange("d"); + String routingKey = "r"; + Binding binding = BindingBuilder.bind(queue).to(directExchange).with(routingKey); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(directExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(routingKey); + } + + @Test + public void directBindingWithQueueName() { + DirectExchange directExchange = new DirectExchange("d"); + Binding binding = BindingBuilder.bind(queue).to(directExchange).withQueueName(); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(directExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(queue.getActualName()); + } + + @Test + public void topicBinding() { + TopicExchange topicExchange = new TopicExchange("t"); + String routingKey = "r"; + Binding binding = BindingBuilder.bind(queue).to(topicExchange).with(routingKey); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(topicExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(routingKey); + } + + @Test + public void headerBinding() { + HeadersExchange headersExchange = new HeadersExchange("h"); + String headerKey = "headerKey"; + Binding binding = BindingBuilder.bind(queue).to(headersExchange).where(headerKey).exists(); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(headersExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(""); + } + + @Test + public void customBinding() { + class CustomExchange extends AbstractExchange { + CustomExchange(String name) { + super(name); + } + + @Override + public String getType() { + return "x-custom"; + } + } + Object argumentObject = new Object(); + CustomExchange customExchange = new CustomExchange("c"); + String routingKey = "r"; + Binding binding = BindingBuilder.// + bind(queue).// + to(customExchange).// + with(routingKey).// + and(Collections.singletonMap("k", argumentObject)); + assertThat(binding).isNotNull(); + assertThat(binding.getArguments().get("k")).isEqualTo(argumentObject); + assertThat(binding.getExchange()).isEqualTo(customExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(routingKey); + } + +} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java new file mode 100644 index 0000000000..832df72ad3 --- /dev/null +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.core; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Björn Michael + * @since 2.4 + */ +public class DeclarablesTests { + + @Test + public void getDeclarables() { + List queues = List.of( + new Queue("q1", false, false, true), + new Queue("q2", false, false, true)); + Declarables declarables = new Declarables(queues); + + assertThat(declarables.getDeclarables()).hasSameElementsAs(queues); + } + + @Test + public void getDeclarablesByType() { + Queue queue = new Queue("queue"); + TopicExchange exchange = new TopicExchange("exchange"); + Binding binding = BindingBuilder.bind(queue).to(exchange).with("foo.bar"); + Declarables declarables = new Declarables(queue, exchange, binding); + + assertThat(declarables.getDeclarablesByType(Queue.class)).containsExactlyInAnyOrder(queue); + assertThat(declarables.getDeclarablesByType(Exchange.class)).containsExactlyInAnyOrder(exchange); + assertThat(declarables.getDeclarablesByType(Binding.class)).containsExactlyInAnyOrder(binding); + assertThat(declarables.getDeclarablesByType(Declarable.class)).containsExactlyInAnyOrder( + queue, exchange, binding); + } + +} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java index 6a1df9d995..7899189b69 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package org.springframework.amqp.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer @@ -30,6 +30,7 @@ * @author Artem Bilan * @author Gary Russell * @author Csaba Soti + * @author Raylax Grey * */ public class MessagePropertiesTests { @@ -53,10 +54,10 @@ public void testReplyToNullByDefault() { @Test public void testDelayHeader() { MessageProperties properties = new MessageProperties(); - Integer delay = 100; - properties.setDelay(delay); + Long delay = 100L; + properties.setDelayLong(delay); assertThat(properties.getHeaders().get(MessageProperties.X_DELAY)).isEqualTo(delay); - properties.setDelay(null); + properties.setDelayLong(null); assertThat(properties.getHeaders().containsKey(MessageProperties.X_DELAY)).isFalse(); } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java index b9eb8b8bd9..83977dcc1a 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; @@ -26,12 +24,15 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Date; +import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.amqp.utils.SerializationUtils; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Mark Fisher * @author Dave Syer @@ -106,9 +107,26 @@ public void fooNotDeserialized() { Message listMessage = new SimpleMessageConverter().toMessage(Collections.singletonList(new Foo()), new MessageProperties()); assertThat(listMessage.toString()).doesNotContainPattern("aFoo"); - Message.addAllowedListPatterns(Foo.class.getName()); - assertThat(message.toString()).contains("aFoo"); - assertThat(listMessage.toString()).contains("aFoo"); + assertThat(message.toString()).contains("[serialized object]"); + assertThat(listMessage.toString()).contains("[serialized object]"); + } + + @Test + void dontToStringLongBody() { + MessageProperties messageProperties = new MessageProperties(); + messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); + StringBuilder builder1 = new StringBuilder(); + IntStream.range(0, 50).forEach(i -> builder1.append("x")); + String bodyAsString = builder1.toString(); + Message message = new Message(bodyAsString.getBytes(), messageProperties); + assertThat(message.toString()).contains(bodyAsString); + StringBuilder builder2 = new StringBuilder(); + IntStream.range(0, 51).forEach(i -> builder2.append("x")); + bodyAsString = builder2.toString(); + message = new Message(bodyAsString.getBytes(), messageProperties); + assertThat(message.toString()).contains("[51]"); + Message.setMaxBodyLength(100); + assertThat(message.toString()).contains(bodyAsString); } @SuppressWarnings("serial") diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/QueueBuilderTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/QueueBuilderTests.java index 93edc1b1a1..7522fe2f4a 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/QueueBuilderTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/QueueBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package org.springframework.amqp.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link QueueBuilder} * diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/QueueNameTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/QueueNameTests.java index e5ac74fa6c..75558a8a14 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/QueueNameTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/QueueNameTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,12 @@ package org.springframework.amqp.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.regex.Pattern; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 1.5.3 diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java index fff6e8a996..da475cc54a 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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,9 @@ package org.springframework.amqp.core.builder; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.ConsistentHashExchange; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.ExchangeBuilder; @@ -29,10 +28,13 @@ import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.core.TopicExchange; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + /** * @author Gary Russell + * @author Artem Bilan * @since 1.6 - * */ public class BuilderTests { @@ -89,6 +91,23 @@ public void testExchangeBuilder() { assertThat(exchange.isDurable()).isTrue(); assertThat(exchange.isInternal()).isFalse(); assertThat(exchange.isDelayed()).isFalse(); + + exchange = ExchangeBuilder.consistentHashExchange("foo") + .ignoreDeclarationExceptions() + .hashHeader("my_header") + .build(); + + assertThat(exchange).isInstanceOf(ConsistentHashExchange.class); + assertThat((String) exchange.getArguments().get("hash-header")).isEqualTo("my_header"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> + ExchangeBuilder.consistentHashExchange("wrong_exchange") + .hashHeader("my_header") + .hashProperty("my_property") + .build()) + .withMessage("The 'hash-header' and 'hash-property' are mutually exclusive."); + } } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/MessageBuilderTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/MessageBuilderTests.java index 1c143da779..e59b149690 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/MessageBuilderTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/MessageBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.core.builder; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -31,6 +29,8 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.MessagePropertiesBuilder; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @author Alex Panchenko diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java deleted file mode 100644 index fc1acdf210..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.remoting; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.AmqpException; -import org.springframework.amqp.core.Address; -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.remoting.client.AmqpProxyFactoryBean; -import org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter; -import org.springframework.amqp.remoting.testhelper.AbstractAmqpTemplate; -import org.springframework.amqp.remoting.testhelper.SentSavingTemplate; -import org.springframework.amqp.remoting.testservice.GeneralException; -import org.springframework.amqp.remoting.testservice.SpecialException; -import org.springframework.amqp.remoting.testservice.TestServiceImpl; -import org.springframework.amqp.remoting.testservice.TestServiceInterface; -import org.springframework.amqp.support.converter.MessageConversionException; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.amqp.support.converter.SimpleMessageConverter; -import org.springframework.remoting.RemoteProxyFailureException; -import org.springframework.remoting.support.RemoteInvocation; - -/** - * @author David Bilge - * @author Artem Bilan - * @author Gary Russell - * @since 1.2 - */ -public class RemotingTest { - - private TestServiceInterface riggedProxy; - - private AmqpInvokerServiceExporter serviceExporter; - - /** - * Set up a rig of directly wired-up proxy and service listener so that both can be tested together without needing - * a running rabbit. - */ - @BeforeEach - public void initializeTestRig() { - // Set up the service - TestServiceInterface testService = new TestServiceImpl(); - this.serviceExporter = new AmqpInvokerServiceExporter(); - final SentSavingTemplate sentSavingTemplate = new SentSavingTemplate(); - this.serviceExporter.setAmqpTemplate(sentSavingTemplate); - this.serviceExporter.setService(testService); - this.serviceExporter.setServiceInterface(TestServiceInterface.class); - - // Set up the client - AmqpProxyFactoryBean amqpProxyFactoryBean = new AmqpProxyFactoryBean(); - amqpProxyFactoryBean.setServiceInterface(TestServiceInterface.class); - AmqpTemplate directForwardingTemplate = new AbstractAmqpTemplate() { - @Override - public Object convertSendAndReceive(Object payload) throws AmqpException { - Object[] arguments = ((RemoteInvocation) payload).getArguments(); - if (arguments.length == 1 && arguments[0].equals("timeout")) { - return null; - } - - MessageConverter messageConverter = serviceExporter.getMessageConverter(); - - Address replyTo = new Address("fakeExchangeName", "fakeRoutingKey"); - MessageProperties messageProperties = new MessageProperties(); - messageProperties.setReplyToAddress(replyTo); - Message message = messageConverter.toMessage(payload, messageProperties); - - serviceExporter.onMessage(message); - - Message resultMessage = sentSavingTemplate.getLastMessage(); - return messageConverter.fromMessage(resultMessage); - } - }; - amqpProxyFactoryBean.setAmqpTemplate(directForwardingTemplate); - amqpProxyFactoryBean.afterPropertiesSet(); - Object rawProxy = amqpProxyFactoryBean.getObject(); - riggedProxy = (TestServiceInterface) rawProxy; - } - - @Test - public void testEcho() { - assertThat(riggedProxy.simpleStringReturningTestMethod("Test")).isEqualTo("Echo Test"); - } - - @Test - public void testSimulatedTimeout() { - try { - this.riggedProxy.simulatedTimeoutMethod("timeout"); - } - catch (RemoteProxyFailureException e) { - assertThat(e.getMessage()).contains("'simulatedTimeoutMethod' with arguments '[timeout]'"); - } - } - - @Test - public void testExceptionPropagation() { - assertThatExceptionOfType(AmqpException.class).isThrownBy(() -> riggedProxy.exceptionThrowingMethod()); - } - - @Test - public void testExceptionReturningMethod() { - assertThatExceptionOfType(GeneralException.class) - .isThrownBy(() -> riggedProxy.notReallyExceptionReturningMethod()); - } - - @Test - public void testActuallyExceptionReturningMethod() { - SpecialException returnedException = riggedProxy.actuallyExceptionReturningMethod(); - assertThat(returnedException).isNotNull(); - } - - @Test - public void testWrongRemoteInvocationArgument() { - MessageConverter messageConverter = this.serviceExporter.getMessageConverter(); - this.serviceExporter.setMessageConverter(new SimpleMessageConverter() { - - private final AtomicBoolean invoked = new AtomicBoolean(); - - @Override - protected Message createMessage(Object object, MessageProperties messageProperties) - throws MessageConversionException { - Message message = super.createMessage(object, messageProperties); - if (!invoked.getAndSet(true)) { - messageProperties.setContentType(null); - } - return message; - } - - }); - - try { - riggedProxy.simpleStringReturningTestMethod("Test"); - } - catch (Exception e) { - assertThat(e).isInstanceOf(IllegalArgumentException.class); - assertThat(e.getMessage()).contains("The message does not contain a RemoteInvocation payload"); - } - - this.serviceExporter.setMessageConverter(messageConverter); - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/AbstractAmqpTemplate.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/AbstractAmqpTemplate.java deleted file mode 100644 index 4de10a97d8..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/AbstractAmqpTemplate.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.remoting.testhelper; - -import org.springframework.amqp.AmqpException; -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessagePostProcessor; -import org.springframework.amqp.core.ReceiveAndReplyCallback; -import org.springframework.amqp.core.ReplyToAddressCallback; -import org.springframework.core.ParameterizedTypeReference; - -/** - * @author David Bilge - * @author Ernest Sadykov - * @since 1.2 - */ -public abstract class AbstractAmqpTemplate implements AmqpTemplate { - - @Override - public void send(Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void send(String routingKey, Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void send(String exchange, String routingKey, Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(String routingKey, Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(String exchange, String routingKey, Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(Object message, MessagePostProcessor messagePostProcessor) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(String routingKey, Object message, MessagePostProcessor messagePostProcessor) - throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(String exchange, String routingKey, Object message, - MessagePostProcessor messagePostProcessor) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message receive() throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message receive(String queueName) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message receive(long timeoutMillis) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message receive(String queueName, long timeoutMillis) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object receiveAndConvert() throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object receiveAndConvert(String queueName) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object receiveAndConvert(long timeoutMillis) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(ReceiveAndReplyCallback callback) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(ReceiveAndReplyCallback callback, String exchange, String routingKey) - throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, String replyExchange, - String replyRoutingKey) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) - throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, - ReplyToAddressCallback replyToAddressCallback) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message sendAndReceive(Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message sendAndReceive(String routingKey, Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message sendAndReceive(String exchange, String routingKey, Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(String routingKey, Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(String exchange, String routingKey, Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(Object message, MessagePostProcessor messagePostProcessor) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(String routingKey, Object message, MessagePostProcessor messagePostProcessor) - throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(String exchange, String routingKey, Object message, - MessagePostProcessor messagePostProcessor) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) - throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) - throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final Object message, final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, ParameterizedTypeReference responseType) - throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final String routingKey, final Object message, ParameterizedTypeReference responseType) throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final Object message, ParameterizedTypeReference responseType) throws AmqpException { - return null; - } - - @Override - public T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { - return null; - } - - @Override - public T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { - return null; - } - - @Override - public T receiveAndConvert(String queueName, ParameterizedTypeReference type) throws AmqpException { - return null; - } - - @Override - public T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException { - return null; - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/SentSavingTemplate.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/SentSavingTemplate.java deleted file mode 100644 index eb6dfe3979..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/SentSavingTemplate.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.remoting.testhelper; - -import org.springframework.amqp.AmqpException; -import org.springframework.amqp.core.Message; - -/** - * @author David Bilge - * @author Gary Russell - * @since 1.2 - */ -public class SentSavingTemplate extends AbstractAmqpTemplate { - - private Message lastMessage = null; - - private String lastExchange = null; - - private String lastRoutingKey = null; - - @Override - public void send(String exchange, String routingKey, Message message) throws AmqpException { - this.lastExchange = exchange; - this.lastRoutingKey = routingKey; - this.lastMessage = message; - } - - public Message getLastMessage() { - return lastMessage; - } - - public String getLastExchange() { - return lastExchange; - } - - public String getLastRoutingKey() { - return lastRoutingKey; - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/GeneralException.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/GeneralException.java deleted file mode 100644 index 7466e8384e..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/GeneralException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.remoting.testservice; - -/** - * @author David Bilge - * @since 1.2 - */ -public class GeneralException extends RuntimeException { - private static final long serialVersionUID = 1763252570120227426L; - - public GeneralException(String message, Throwable cause) { - super(message, cause); - } - - public GeneralException(String message) { - super(message); - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceImpl.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceImpl.java deleted file mode 100644 index 6e758d6536..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceImpl.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.remoting.testservice; - -import org.springframework.amqp.AmqpException; - -/** - * @author David Bilge - * @author Gary Russell - * @since 1.2 - */ -public class TestServiceImpl implements TestServiceInterface { - @Override - public void simpleTestMethod() { - // Do nothing - } - - @Override - public String simpleStringReturningTestMethod(String string) { - return "Echo " + string; - } - - @Override - public void exceptionThrowingMethod() { - throw new AmqpException("This is an exception"); - } - - @Override - public Object echo(Object o) { - return o; - } - - @Override - public SpecialException notReallyExceptionReturningMethod() { - throw new GeneralException("This exception should not be interpreted as a return type but be thrown instead."); - } - - @Override - public SpecialException actuallyExceptionReturningMethod() { - return new SpecialException("This exception should not be thrown on the client side but just be returned!"); - } - - @Override - public Object simulatedTimeoutMethod(Object o) { - return null; - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/AmqpMessageHeaderAccessorTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/AmqpMessageHeaderAccessorTests.java index 3368346e5a..6a7d2b009c 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/AmqpMessageHeaderAccessorTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/AmqpMessageHeaderAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.support; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - import java.util.Date; import java.util.Map; @@ -30,6 +27,9 @@ import org.springframework.messaging.support.MessageBuilder; import org.springframework.util.MimeType; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + /** * @author Stephane Nicoll * @author Gary Russell diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/MessagePostProcessorUtilsTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/MessagePostProcessorUtilsTests.java index 563c2b1db9..7a28939d39 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/MessagePostProcessorUtilsTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/MessagePostProcessorUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.support; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.Collection; import java.util.Iterator; @@ -31,6 +29,8 @@ import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 1.4.2 diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java index 2e9c768279..b39a0eccf5 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.support; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -33,17 +30,22 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.util.MimeTypeUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; + /** * @author Mark Fisher * @author Gary Russell * @author Oleg Zhurakousky + * @author Raylax Grey */ public class SimpleAmqpHeaderMapperTests { @Test public void fromHeaders() { SimpleAmqpHeaderMapper headerMapper = new SimpleAmqpHeaderMapper(); - Map headerMap = new HashMap(); + Map headerMap = new HashMap<>(); headerMap.put(AmqpHeaders.APP_ID, "test.appId"); headerMap.put(AmqpHeaders.CLUSTER_ID, "test.clusterId"); headerMap.put(AmqpHeaders.CONTENT_ENCODING, "test.contentEncoding"); @@ -51,7 +53,7 @@ public void fromHeaders() { headerMap.put(AmqpHeaders.CONTENT_TYPE, "test.contentType"); String testCorrelationId = "foo"; headerMap.put(AmqpHeaders.CORRELATION_ID, testCorrelationId); - headerMap.put(AmqpHeaders.DELAY, 1234); + headerMap.put(AmqpHeaders.DELAY, 1234L); headerMap.put(AmqpHeaders.DELIVERY_MODE, MessageDeliveryMode.NON_PERSISTENT); headerMap.put(AmqpHeaders.DELIVERY_TAG, 1234L); headerMap.put(AmqpHeaders.EXPIRATION, "test.expiration"); @@ -92,13 +94,39 @@ public void fromHeaders() { assertThat(amqpProperties.getTimestamp()).isEqualTo(testTimestamp); assertThat(amqpProperties.getType()).isEqualTo("test.type"); assertThat(amqpProperties.getUserId()).isEqualTo("test.userId"); - assertThat(amqpProperties.getDelay()).isEqualTo(Integer.valueOf(1234)); + assertThat(amqpProperties.getDelayLong()).isEqualTo(Long.valueOf(1234)); } + @Test + public void fromHeadersWithLongDelay() { + SimpleAmqpHeaderMapper headerMapper = new SimpleAmqpHeaderMapper(); + Map headerMap = new HashMap<>(); + headerMap.put(AmqpHeaders.DELAY, 1234L); + MessageHeaders messageHeaders = new MessageHeaders(headerMap); + MessageProperties amqpProperties = new MessageProperties(); + headerMapper.fromHeaders(messageHeaders, amqpProperties); + assertThat(amqpProperties.getDelayLong()).isEqualTo(Long.valueOf(1234)); + + amqpProperties.setDelayLong(5678L); + assertThat(amqpProperties.getDelayLong()).isEqualTo(Long.valueOf(5678)); + + amqpProperties.setDelayLong(null); + assertThat(amqpProperties.getHeaders().containsKey(AmqpHeaders.DELAY)).isFalse(); + + amqpProperties.setDelayLong(MessageProperties.X_DELAY_MAX); + assertThat(amqpProperties.getDelayLong()).isEqualTo(Long.valueOf(MessageProperties.X_DELAY_MAX)); + + assertThatThrownBy(() -> amqpProperties.setDelayLong(MessageProperties.X_DELAY_MAX + 1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Delay cannot exceed"); + + } + + @Test public void fromHeadersWithContentTypeAsMediaType() { SimpleAmqpHeaderMapper headerMapper = new SimpleAmqpHeaderMapper(); - Map headerMap = new HashMap(); + Map headerMap = new HashMap<>(); headerMap.put(AmqpHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_HTML); @@ -126,7 +154,7 @@ public void toHeaders() { amqpProperties.setMessageCount(42); amqpProperties.setMessageId("test.messageId"); amqpProperties.setPriority(22); - amqpProperties.setReceivedDelay(1234); + amqpProperties.setReceivedDelayLong(1234L); amqpProperties.setReceivedExchange("test.receivedExchange"); amqpProperties.setReceivedRoutingKey("test.receivedRoutingKey"); amqpProperties.setRedelivered(true); @@ -151,7 +179,7 @@ public void toHeaders() { assertThat(headerMap.get(AmqpHeaders.EXPIRATION)).isEqualTo("test.expiration"); assertThat(headerMap.get(AmqpHeaders.MESSAGE_COUNT)).isEqualTo(42); assertThat(headerMap.get(AmqpHeaders.MESSAGE_ID)).isEqualTo("test.messageId"); - assertThat(headerMap.get(AmqpHeaders.RECEIVED_DELAY)).isEqualTo(1234); + assertThat(headerMap.get(AmqpHeaders.RECEIVED_DELAY)).isEqualTo(1234L); assertThat(headerMap.get(AmqpHeaders.RECEIVED_EXCHANGE)).isEqualTo("test.receivedExchange"); assertThat(headerMap.get(AmqpHeaders.RECEIVED_ROUTING_KEY)).isEqualTo("test.receivedRoutingKey"); assertThat(headerMap.get(AmqpHeaders.REPLY_TO)).isEqualTo("test.replyTo"); @@ -170,7 +198,7 @@ public void jsonTypeIdNotOverwritten() { Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); MessageProperties amqpProperties = new MessageProperties(); converter.toMessage("123", amqpProperties); - Map headerMap = new HashMap(); + Map headerMap = new HashMap<>(); headerMap.put("__TypeId__", "java.lang.Integer"); MessageHeaders messageHeaders = new MessageHeaders(headerMap); headerMapper.fromHeaders(messageHeaders, amqpProperties); diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java index dfb3d1709c..32d96caf02 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.io.Serializable; import java.util.Collections; @@ -28,6 +25,9 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.util.Assert; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Gary Russell * @since 1.5.5 @@ -40,7 +40,11 @@ public void testAllowedList() throws Exception { SerializerMessageConverter converter = new SerializerMessageConverter(); TestBean testBean = new TestBean("foo"); Message message = converter.toMessage(testBean, new MessageProperties()); - Object fromMessage = converter.fromMessage(message); + // when env var not set +// assertThatExceptionOfType(SecurityException.class).isThrownBy(() -> converter.fromMessage(message)); + Object fromMessage; + // when env var set. + fromMessage = converter.fromMessage(message); assertThat(fromMessage).isEqualTo(testBean); converter.setAllowedListPatterns(Collections.singletonList("*")); @@ -54,15 +58,8 @@ public void testAllowedList() throws Exception { fromMessage = converter.fromMessage(message); assertThat(fromMessage).isEqualTo(testBean); - try { - converter.setAllowedListPatterns(Collections.singletonList("foo.*")); - fromMessage = converter.fromMessage(message); - assertThat(fromMessage).isEqualTo(testBean); - fail("Expected SecurityException"); - } - catch (SecurityException e) { - - } + converter.setAllowedListPatterns(Collections.singletonList("foo.*")); + assertThatExceptionOfType(SecurityException.class).isThrownBy(() -> converter.fromMessage(message)); } @SuppressWarnings("serial") @@ -77,7 +74,7 @@ protected TestBean(String text) { @Override public boolean equals(Object other) { - return (other instanceof TestBean && this.text.equals(((TestBean) other).text)); + return (other instanceof TestBean testBean && this.text.equals(testBean.text)); } @Override diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java index 731d92546a..c76a30f052 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,17 @@ package org.springframework.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.io.Serializable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @author Artem Bilan @@ -35,6 +36,16 @@ */ public class ContentTypeDelegatingMessageConverterTests { + @BeforeAll + static void setUp() { + System.setProperty("spring.amqp.deserialization.trust.all", "true"); + } + + @AfterAll + static void tearDown() { + System.setProperty("spring.amqp.deserialization.trust.all", "false"); + } + @Test public void testDelegationOutbound() { ContentTypeDelegatingMessageConverter converter = new ContentTypeDelegatingMessageConverter(); @@ -56,16 +67,6 @@ public void testDelegationOutbound() { assertThat(new String(message.getBody())).isEqualTo("{\"foo\":\"bar\"}"); converted = converter.fromMessage(message); assertThat(converted).isInstanceOf(Foo.class); - - converter = new ContentTypeDelegatingMessageConverter(null); // no default - try { - converter.toMessage(foo, props); - fail("Expected exception"); - } - catch (Exception e) { - assertThat(e).isInstanceOf(MessageConversionException.class); - assertThat(e.getMessage()).contains("No delegate converter"); - } } @SuppressWarnings("serial") diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultClassMapperTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultClassMapperTests.java index 74e90ca538..941feddbaf 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultClassMapperTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultClassMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; - import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -30,6 +27,9 @@ import org.springframework.amqp.core.MessageProperties; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + /** * @author James Carr * @author Gary Russell diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapperTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapperTests.java index 4f2f43ed49..348b93fbfa 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapperTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package org.springframework.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.BDDMockito.given; - import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.fasterxml.jackson.databind.type.MapType; +import com.fasterxml.jackson.databind.type.TypeFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,10 +32,9 @@ import org.springframework.amqp.core.MessageProperties; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.type.CollectionType; -import com.fasterxml.jackson.databind.type.MapType; -import com.fasterxml.jackson.databind.type.TypeFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.BDDMockito.given; /** * @author James Carr diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java index 19ec20ea4b..126b32ef9e 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.IOException; import java.math.BigDecimal; import java.util.Hashtable; @@ -25,6 +23,13 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,15 +38,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.data.web.JsonPath; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.util.MimeTypeUtils; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Mark Pollack @@ -52,6 +54,7 @@ * @author Artem Bilan */ @SpringJUnitConfig +@DirtiesContext public class Jackson2JsonMessageConverterTests { public static final String TRUSTED_PACKAGE = Jackson2JsonMessageConverterTests.class.getPackage().getName(); @@ -70,7 +73,7 @@ public void before() { trade.setAccountName("Acct1"); trade.setBuyRequest(true); trade.setOrderType("Market"); - trade.setPrice(new BigDecimal(103.30)); + trade.setPrice(new BigDecimal("103.30")); trade.setQuantity(100); trade.setRequestId("R123"); trade.setTicker("VMW"); @@ -296,7 +299,6 @@ public void testMissingContentType() { Object foo = j2Converter.fromMessage(message); assertThat(foo).isInstanceOf(Foo.class); - messageProperties.setContentType(null); foo = j2Converter.fromMessage(message); assertThat(foo).isInstanceOf(Foo.class); @@ -399,6 +401,67 @@ void concreteInMapRegression() throws Exception { assertThat(foos.values().iterator().next().getField()).isEqualTo("baz"); } + @Test + void charsetInContentType() { + trade.setUserName("John Doe ∫"); + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + String utf8 = "application/json;charset=utf-8"; + converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf8)); + Message message = converter.toMessage(trade, new MessageProperties()); + int bodyLength8 = message.getBody().length; + assertThat(message.getMessageProperties().getContentEncoding()).isNull(); + assertThat(message.getMessageProperties().getContentType()).isEqualTo(utf8); + SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + // use content type property + String utf16 = "application/json;charset=utf-16"; + converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); + message = converter.toMessage(trade, new MessageProperties()); + assertThat(message.getBody().length).isNotEqualTo(bodyLength8); + assertThat(message.getMessageProperties().getContentEncoding()).isNull(); + assertThat(message.getMessageProperties().getContentType()).isEqualTo(utf16); + marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + // no encoding in message, use configured default + converter.setSupportedContentType(MimeTypeUtils.parseMimeType("application/json")); + converter.setDefaultCharset("UTF-16"); + message = converter.toMessage(trade, new MessageProperties()); + assertThat(message.getBody().length).isNotEqualTo(bodyLength8); + assertThat(message.getMessageProperties().getContentEncoding()).isNotNull(); + message.getMessageProperties().setContentEncoding(null); + marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + } + + @Test + void noConfigForCharsetInContentType() { + trade.setUserName("John Doe ∫"); + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + Message message = converter.toMessage(trade, new MessageProperties()); + int bodyLength8 = message.getBody().length; + SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + // no encoding in message; use configured default + message = converter.toMessage(trade, new MessageProperties()); + assertThat(message.getMessageProperties().getContentEncoding()).isNotNull(); + message.getMessageProperties().setContentEncoding(null); + marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + converter.setDefaultCharset("UTF-16"); + Message message2 = converter.toMessage(trade, new MessageProperties()); + message2.getMessageProperties().setContentEncoding(null); + assertThat(message2.getBody().length).isNotEqualTo(bodyLength8); + converter.setDefaultCharset("UTF-8"); + + assertThatExceptionOfType(MessageConversionException.class).isThrownBy( + () -> converter.fromMessage(message2)); + } + public List fooLister() { return null; } @@ -606,6 +669,7 @@ public void setField(String field) { @SuppressWarnings("serial") public static class BazModule extends SimpleModule { + @SuppressWarnings("this-escape") public BazModule() { addDeserializer(Baz.class, new BazDeserializer()); } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java index d5e3825a0c..b1a97acf75 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package org.springframework.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; - import java.math.BigDecimal; import java.util.Hashtable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,10 +31,10 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Mohammad Hewedy @@ -43,6 +43,7 @@ * @since 2.1 */ @SpringJUnitConfig +@DirtiesContext public class Jackson2XmlMessageConverterTests { public static final String TRUSTED_PACKAGE = Jackson2XmlMessageConverterTests.class.getPackage().getName(); diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MarshallingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MarshallingMessageConverterTests.java index bae3eb14d9..70d8972109 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MarshallingMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MarshallingMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.IOException; import javax.xml.transform.Result; @@ -33,6 +31,8 @@ import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.XmlMappingException; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Mark Fisher * @author James Carr diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java index 3482f75b02..984bba3de2 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,15 @@ package org.springframework.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageProperties; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + /** * @author Stephane Nicoll @@ -49,11 +49,6 @@ public void toMessageWithTextMessage() { assertThat(new String(message.getBody())).isEqualTo("Hello World"); } - @Test - public void fromNull() { - assertThat(converter.fromMessage(null)).isNull(); - } - @Test public void customPayloadConverter() throws Exception { converter.setPayloadConverter(new SimpleMessageConverter() { diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java index 74eb71cf96..9ec8889118 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,40 +16,35 @@ package org.springframework.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.InputStream; +import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.utils.test.TestUtils; -import org.springframework.core.NestedIOException; -import org.springframework.core.serializer.DefaultDeserializer; -import org.springframework.core.serializer.Deserializer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan */ public class SerializerMessageConverterTests extends AllowedListDeserializingMessageConverterTests { @Test - public void bytesAsDefaultMessageBodyType() throws Exception { + public void bytesAsDefaultMessageBodyType() { SerializerMessageConverter converter = new SerializerMessageConverter(); Message message = new Message("test".getBytes(), new MessageProperties()); Object result = converter.fromMessage(message); assertThat(result.getClass()).isEqualTo(byte[].class); - assertThat(new String((byte[]) result, "UTF-8")).isEqualTo("test"); + assertThat(new String((byte[]) result, StandardCharsets.UTF_8)).isEqualTo("test"); } @Test @@ -65,7 +60,7 @@ public void messageToString() { @Test public void messageToBytes() { SerializerMessageConverter converter = new SerializerMessageConverter(); - Message message = new Message(new byte[] { 1, 2, 3 }, new MessageProperties()); + Message message = new Message(new byte[]{ 1, 2, 3 }, new MessageProperties()); message.getMessageProperties().setContentType(MessageProperties.CONTENT_TYPE_BYTES); Object result = converter.fromMessage(message); assertThat(result.getClass()).isEqualTo(byte[].class); @@ -126,7 +121,7 @@ public void stringToMessage() throws Exception { @Test public void bytesToMessage() throws Exception { SerializerMessageConverter converter = new SerializerMessageConverter(); - Message message = converter.toMessage(new byte[] { 1, 2, 3 }, new MessageProperties()); + Message message = converter.toMessage(new byte[]{ 1, 2, 3 }, new MessageProperties()); String contentType = message.getMessageProperties().getContentType(); byte[] body = message.getBody(); assertThat(contentType).isEqualTo("application/octet-stream"); @@ -149,26 +144,8 @@ public void serializedObjectToMessage() throws Exception { assertThat(deserializedObject).isEqualTo(testBean); } - @SuppressWarnings("unchecked") - @Test - public void testDefaultDeserializerClassLoader() throws Exception { - SerializerMessageConverter converter = new SerializerMessageConverter(); - ClassLoader loader = mock(ClassLoader.class); - Deserializer deserializer = new DefaultDeserializer(loader); - converter.setDeserializer(deserializer); - assertThat(TestUtils.getPropertyValue(converter, "defaultDeserializerClassLoader")).isSameAs(loader); - assertThat(TestUtils.getPropertyValue(converter, "usingDefaultDeserializer", Boolean.class)).isTrue(); - Deserializer mock = mock(Deserializer.class); - converter.setDeserializer(mock); - assertThat(TestUtils.getPropertyValue(converter, "usingDefaultDeserializer", Boolean.class)).isFalse(); - TestBean testBean = new TestBean("foo"); - Message message = converter.toMessage(testBean, new MessageProperties()); - converter.fromMessage(message); - verify(mock).deserialize(Mockito.any(InputStream.class)); - } - @Test - public void messageConversionExceptionForClassNotFound() throws Exception { + public void messageConversionExceptionForClassNotFound() { SerializerMessageConverter converter = new SerializerMessageConverter(); TestBean testBean = new TestBean("foo"); Message message = converter.toMessage(testBean, new MessageProperties()); @@ -177,8 +154,8 @@ public void messageConversionExceptionForClassNotFound() throws Exception { byte[] body = message.getBody(); body[10] = 'z'; assertThatThrownBy(() -> converter.fromMessage(message)) - .isExactlyInstanceOf(MessageConversionException.class) - .hasCauseExactlyInstanceOf(NestedIOException.class); + .isExactlyInstanceOf(MessageConversionException.class) + .hasCauseExactlyInstanceOf(IOException.class); } } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SimpleMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SimpleMessageConverterTests.java index ec6e72d9fb..e491ae377d 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SimpleMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SimpleMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,6 @@ package org.springframework.amqp.support.converter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; @@ -30,6 +26,10 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; + /** * @author Mark Fisher * @author Gary Russell diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java new file mode 100644 index 0000000000..215e07a593 --- /dev/null +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.junit; + +import java.io.IOException; +import java.time.Duration; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +/** + * @author Gary Russell + * @author Artem Bilan + * + * @since 2.4 + * + */ +@Testcontainers(disabledWithoutDocker = true) +public abstract class AbstractTestContainerTests { + + private static final Log LOG = LogFactory.getLog(AbstractTestContainerTests.class); + + protected static final @Nullable RabbitMQContainer RABBITMQ; + + static { + if (System.getProperty("spring.rabbit.use.local.server") == null + && System.getenv("SPRING_RABBIT_USE_LOCAL_SERVER") == null) { + String image = "rabbitmq:management"; + String cache = System.getenv().get("IMAGE_CACHE"); + if (cache != null) { + image = cache + image; + } + DockerImageName imageName = DockerImageName.parse(image) + .asCompatibleSubstituteFor("rabbitmq"); + RABBITMQ = new RabbitMQContainer(imageName) + .withExposedPorts(5672, 15672, 5552) + .withStartupTimeout(Duration.ofMinutes(2)); + } + else { + RABBITMQ = null; + } + } + + @BeforeAll + static void startContainer() throws IOException, InterruptedException { + if (RABBITMQ != null) { + RABBITMQ.start(); + RABBITMQ.execInContainer("rabbitmq-plugins", "enable", "rabbitmq_stream"); + } + else { + LOG.info("The local RabbitMQ broker will be used instead of Testcontainers."); + } + } + + public static int amqpPort() { + return RABBITMQ != null ? RABBITMQ.getAmqpPort() : 5672; + } + + public static int managementPort() { + return RABBITMQ != null ? RABBITMQ.getMappedPort(15672) : 15672; + } + + public static int streamPort() { + return RABBITMQ != null ? RABBITMQ.getMappedPort(5552) : 5552; + } + + public static String restUri() { + return RABBITMQ != null ? RABBITMQ.getHttpUrl() + "/api/" : "http://localhost:" + managementPort() + "/api/"; + } + +} diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunning.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunning.java index 1f9ba0b9df..86dea5aefb 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunning.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunning.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,9 @@ package org.springframework.amqp.rabbit.junit; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeNoException; - import java.util.Map; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.rules.TestWatcher; @@ -29,7 +27,8 @@ import org.springframework.amqp.rabbit.junit.BrokerRunningSupport.BrokerNotAliveException; -import com.rabbitmq.client.ConnectionFactory; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeNoException; /** * A rule that prevents integration tests from failing if the Rabbit broker application is diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java index 2852ef2857..54f4c5d94d 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,26 +17,36 @@ package org.springframework.amqp.rabbit.junit; import java.io.IOException; +import java.io.Serial; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URI; import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeoutException; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.util.Base64Utils; +import org.springframework.http.HttpStatus; import org.springframework.util.StringUtils; - -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.http.client.Client; +import org.springframework.web.util.UriUtils; /** * A class that can be used to prevent integration tests from failing if the Rabbit broker application is @@ -50,6 +60,7 @@ * * @author Dave Syer * @author Gary Russell + * @author Artem Bilan * * @since 2.2 */ @@ -88,15 +99,15 @@ public final class BrokerRunningSupport { private final boolean management; - private final String[] queues; + private final String @Nullable [] queues; private int port; private String hostName = fromEnvironment(BROKER_HOSTNAME, "localhost"); - private String adminUri = fromEnvironment(BROKER_ADMIN_URI, null); + private @Nullable String adminUri = fromEnvironment(BROKER_ADMIN_URI); - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; private String user = fromEnvironment(BROKER_USER, GUEST); @@ -108,6 +119,19 @@ public final class BrokerRunningSupport { private boolean purgeAfterEach; + private static @Nullable String fromEnvironment(String key) { + String environmentValue = ENVIRONMENT_OVERRIDES.get(key); + if (!StringUtils.hasText(environmentValue)) { + environmentValue = System.getenv(key); + } + if (StringUtils.hasText(environmentValue)) { + return environmentValue; + } + else { + return null; + } + } + private static String fromEnvironment(String key, String defaultValue) { String environmentValue = ENVIRONMENT_OVERRIDES.get(key); if (!StringUtils.hasText(environmentValue)) { @@ -143,7 +167,6 @@ public static void clearEnvironmentVariableOverrides() { /** * Ensure the broker is running and has a empty queue(s) with the specified name(s) in the * default exchange. - * * @param names the queues to declare for the test. * @return a new rule that assumes an existing running broker */ @@ -177,7 +200,7 @@ public static BrokerRunningSupport isBrokerAndManagementRunning() { * @return a new rule that assumes an existing broker with the management plugin with * the provided queues declared (and emptied if needed).. */ - public static BrokerRunningSupport isBrokerAndManagementRunningWithEmptyQueues(String...queues) { + public static BrokerRunningSupport isBrokerAndManagementRunningWithEmptyQueues(String... queues) { return new BrokerRunningSupport(true, true, queues); } @@ -185,7 +208,7 @@ private BrokerRunningSupport(boolean purge, String... queues) { this(purge, false, queues); } - BrokerRunningSupport(boolean purge, boolean management, String... queues) { + BrokerRunningSupport(boolean purge, boolean management, String @Nullable ... queues) { if (queues != null) { this.queues = Arrays.copyOf(queues, queues.length); } @@ -194,9 +217,10 @@ private BrokerRunningSupport(boolean purge, String... queues) { } this.purge = purge; this.management = management; - setPort(fromEnvironment(BROKER_PORT, null) == null + String portFromEnvironment = fromEnvironment(BROKER_PORT); + setPort(portFromEnvironment == null ? BrokerTestUtils.getPort() - : Integer.valueOf(fromEnvironment(BROKER_PORT, null))); + : Integer.parseInt(portFromEnvironment)); } private BrokerRunningSupport(boolean assumeOnline) { @@ -305,7 +329,6 @@ public String getAdminPassword() { return this.adminPassword; } - public boolean isPurgeAfterEach() { return this.purgeAfterEach; } @@ -355,32 +378,70 @@ private Channel createQueues(Connection connection) throws IOException, URISynta Channel channel; channel = connection.createChannel(); - for (String queueName : this.queues) { - - if (this.purge) { - LOGGER.debug("Deleting queue: " + queueName); - // Delete completely - gets rid of consumers and bindings as well - channel.queueDelete(queueName); - } - - if (isDefaultQueue(queueName)) { - // Just for test probe. - channel.queueDelete(queueName); - } - else { - channel.queueDeclare(queueName, true, false, false, null); + if (this.queues != null) { + for (String queueName : this.queues) { + if (this.purge) { + LOGGER.debug("Deleting queue: " + queueName); + // Delete completely - gets rid of consumers and bindings as well + channel.queueDelete(queueName); + } + + if (isDefaultQueue(queueName)) { + // Just for test probe. + channel.queueDelete(queueName); + } + else { + channel.queueDeclare(queueName, true, false, false, null); + } } } + if (this.management) { - Client client = new Client(getAdminUri(), this.adminUser, this.adminPassword); - if (!client.alivenessTest("/")) { - throw new BrokerNotAliveException("Aliveness test failed for localhost:15672 guest/quest; " - + "management not available"); - } + alivenessTest(); } return channel; } + private void alivenessTest() throws URISyntaxException { + HttpClient client = HttpClient.newBuilder() + .authenticator(new Authenticator() { + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(getAdminUser(), getAdminPassword().toCharArray()); + } + + }) + .build(); + URI uri = new URI(getAdminUri()) + .resolve("/api/aliveness-test/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8)); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(uri) + .build(); + HttpResponse response; + try { + response = client.send(request, BodyHandlers.ofString()); + } + catch (IOException ex) { + throw new BrokerNotAliveException("Failed to check aliveness", ex); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new BrokerNotAliveException("Interrupted while checking aliveness", ex); + } + String body = null; + if (response.statusCode() == HttpStatus.OK.value()) { + body = response.body(); + } + if (body == null || !body.contentEquals("{\"status\":\"ok\"}")) { + throw new BrokerNotAliveException("Aliveness test failed for " + uri + + " user: " + getAdminUser() + " pw: " + getAdminPassword() + + " status: " + response.statusCode() + " body: " + body + + "; management not available"); + } + } + public static boolean fatal() { String serversRequired = System.getenv(BROKER_REQUIRED); if (Boolean.parseBoolean(serversRequired)) { @@ -401,8 +462,8 @@ public String generateId() { UUID uuid = UUID.randomUUID(); ByteBuffer bb = ByteBuffer.wrap(new byte[SIXTEEN]); bb.putLong(uuid.getMostSignificantBits()) - .putLong(uuid.getLeastSignificantBits()); - return "SpringBrokerRunning." + Base64Utils.encodeToUrlSafeString(bb.array()).replaceAll("=", ""); + .putLong(uuid.getLeastSignificantBits()); + return "SpringBrokerRunning." + Base64.getUrlEncoder().encodeToString(bb.array()).replaceAll("=", ""); } private boolean isDefaultQueue(String queue) { @@ -415,8 +476,8 @@ private boolean isDefaultQueue(String queue) { * @param additionalQueues additional queues to remove that might have been created by * tests. */ - public void removeTestQueues(String... additionalQueues) { - List queuesToRemove = Arrays.asList(this.queues); + public void removeTestQueues(String @Nullable ... additionalQueues) { + List queuesToRemove = this.queues != null ? Arrays.asList(this.queues) : new ArrayList<>(); if (additionalQueues != null) { queuesToRemove = new ArrayList<>(queuesToRemove); queuesToRemove.addAll(Arrays.asList(additionalQueues)); @@ -576,7 +637,7 @@ public String getAdminUri() { return this.adminUri; } - private void closeResources(Connection connection, Channel channel) { + private void closeResources(@Nullable Connection connection, @Nullable Channel channel) { if (channel != null) { try { channel.close(); @@ -601,6 +662,7 @@ private void closeResources(Connection connection, Channel channel) { */ public static class BrokerNotAliveException extends RuntimeException { + @Serial private static final long serialVersionUID = 1L; BrokerNotAliveException(String message) { diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java index 649476155e..35706b0dc3 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author 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.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -29,7 +28,6 @@ import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.LoggerConfig; -import org.slf4j.LoggerFactory; /** * Utility methods for JUnit rules and conditions. @@ -53,7 +51,7 @@ private JUnitUtils() { * @return the parsed property value if it exists, false otherwise. */ public static boolean parseBooleanProperty(String property) { - for (String value : new String[] { System.getenv(property), System.getProperty(property) }) { + for (String value : new String[] {System.getenv(property), System.getProperty(property)}) { if (Boolean.parseBoolean(value)) { return true; } @@ -109,17 +107,18 @@ public static LevelsContainer adjustLogLevels(String methodName, List> ctx.updateLoggers(); Map oldLbLevels = new HashMap<>(); - categories.forEach(cat -> { - ch.qos.logback.classic.Logger lbLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(cat); - oldLbLevels.put(cat, lbLogger.getLevel()); - lbLogger.setLevel(ch.qos.logback.classic.Level.toLevel(level.name())); - }); +// TODO: Fix +// categories.forEach(cat -> { +// ch.qos.logback.classic.Logger lbLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(cat); +// oldLbLevels.put(cat, lbLogger.getLevel()); +// lbLogger.setLevel(ch.qos.logback.classic.Level.toLevel(level.name())); +// }); LOGGER.info("++++++++++++++++++++++++++++ " + "Overridden log level setting for: " + classes.stream() .map(Class::getSimpleName) - .collect(Collectors.toList()) - + " and " + categories.toString() + .toList() + + " and " + categories + " for test " + methodName); return new LevelsContainer(classLevels, categoryLevels, oldLbLevels); } @@ -137,8 +136,8 @@ public static void revertLevels(String methodName, LevelsContainer container) { ((Logger) LogManager.getLogger(key)).setLevel(value); } }); - container.oldLbLevels.forEach((key, value) -> - ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(key)).setLevel(value)); +// container.oldLbLevels.forEach((key, value) -> +// ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(key)).setLevel(value)); } public static class LevelsContainer { diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/LongRunning.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/LongRunning.java index f2e3a5d85c..9ea1221489 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/LongRunning.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/LongRunning.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ public @interface LongRunning { /** - * The name of the variable/property used to determine whether long runnning tests + * The name of the variable/property used to determine whether long running tests * should run. * @return the name of the variable/property. */ diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCondition.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCondition.java index 5849667783..bbb0fa4704 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCondition.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 the original author 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.AnnotatedElement; import java.util.Optional; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.ConditionEvaluationResult; @@ -33,8 +34,6 @@ import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.Assert; -import com.rabbitmq.client.ConnectionFactory; - /** * JUnit5 {@link ExecutionCondition}. Looks for {@code @RabbitAvailable} annotated classes * and disables if found the broker is not available. diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/package-info.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/package-info.java index a57318ae9b..ed6deb88c2 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/package-info.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/package-info.java @@ -2,4 +2,5 @@ * Provides support classes (Rules etc. with no spring-rabbit dependencies) for JUnit * tests. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.junit; diff --git a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/BrokerRunningTests.java b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/BrokerRunningTests.java index 60e73344d7..0f387ecc57 100644 --- a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/BrokerRunningTests.java +++ b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/BrokerRunningTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,15 @@ package org.springframework.amqp.rabbit.junit; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.HashMap; import java.util.Map; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.Test; import org.springframework.beans.DirectFieldAccessor; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCTORInjectionTests.java b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCTORInjectionTests.java index 55f643f288..84ea5af2c8 100644 --- a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCTORInjectionTests.java +++ b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCTORInjectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,13 @@ package org.springframework.amqp.rabbit.junit; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; - import com.rabbitmq.client.AMQP.Queue.DeclareOk; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java index 9c93f81d3f..9dedf0a7f4 100644 --- a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java +++ b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 the original author or 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,20 @@ package org.springframework.amqp.rabbit.junit; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; - import com.rabbitmq.client.AMQP.Queue.DeclareOk; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell * @since 2.0.2 * */ -@RabbitAvailable(queues = "rabbitAvailableTests.queue") +@RabbitAvailable(queues = "rabbitAvailableTests.queue", management = true) public class RabbitAvailableTests { @Test diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java index 7d24a61f32..5e64318ec6 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,23 @@ import java.lang.reflect.Method; +import com.rabbitmq.stream.Environment; +import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.config.BaseRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.rabbit.listener.MethodRabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; -import org.springframework.lang.Nullable; +import org.springframework.amqp.utils.JavaUtils; import org.springframework.rabbit.stream.listener.ConsumerCustomizer; import org.springframework.rabbit.stream.listener.StreamListenerContainer; import org.springframework.rabbit.stream.listener.adapter.StreamMessageListenerAdapter; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservationConvention; import org.springframework.util.Assert; -import com.rabbitmq.stream.Environment; - /** * Factory for StreamListenerContainer. * @@ -46,9 +49,11 @@ public class StreamRabbitListenerContainerFactory private boolean nativeListener; - private ConsumerCustomizer consumerCustomizer; + private @Nullable ConsumerCustomizer consumerCustomizer; + + private @Nullable ContainerCustomizer containerCustomizer; - private ContainerCustomizer containerCustomizer; + private @Nullable RabbitStreamListenerObservationConvention streamListenerObservationConvention; /** * Construct an instance using the provided environment. @@ -84,21 +89,39 @@ public void setContainerCustomizer(ContainerCustomizer this.containerCustomizer = containerCustomizer; } + /** + * Set a {@link RabbitStreamListenerObservationConvention} that is used when receiving + * native stream messages. + * @param streamListenerObservationConvention the convention. + * @since 3.0.5 + */ + public void setStreamListenerObservationConvention( + RabbitStreamListenerObservationConvention streamListenerObservationConvention) { + + this.streamListenerObservationConvention = streamListenerObservationConvention; + } + @Override - public StreamListenerContainer createListenerContainer(RabbitListenerEndpoint endpoint) { - if (endpoint instanceof MethodRabbitListenerEndpoint && this.nativeListener) { - ((MethodRabbitListenerEndpoint) endpoint).setAdapterProvider( - (boolean batch, Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) -> { + public StreamListenerContainer createListenerContainer(@Nullable RabbitListenerEndpoint endpoint) { + if (endpoint instanceof MethodRabbitListenerEndpoint methodRabbitListenerEndpoint && this.nativeListener) { + methodRabbitListenerEndpoint.setAdapterProvider( + (boolean batch, @Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler, + @Nullable BatchingStrategy batchingStrategy) -> { Assert.isTrue(!batch, "Batch listeners are not supported by the stream container"); return new StreamMessageListenerAdapter(bean, method, returnExceptions, errorHandler); }); } StreamListenerContainer container = createContainerInstance(); - if (this.consumerCustomizer != null) { - container.setConsumerCustomizer(this.consumerCustomizer); - } + Advice[] adviceChain = getAdviceChain(); + JavaUtils.INSTANCE + .acceptIfNotNull(getApplicationContext(), container::setApplicationContext) + .acceptIfNotNull(this.consumerCustomizer, container::setConsumerCustomizer) + .acceptIfNotNull(adviceChain, container::setAdviceChain) + .acceptIfNotNull(getMicrometerEnabled(), container::setMicrometerEnabled) + .acceptIfNotNull(getObservationEnabled(), container::setObservationEnabled) + .acceptIfNotNull(this.streamListenerObservationConvention, container::setObservationConvention); applyCommonOverrides(endpoint, container); if (this.containerCustomizer != null) { this.containerCustomizer.configure(container); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java new file mode 100644 index 0000000000..52df70552f --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java @@ -0,0 +1,123 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.Binding.DestinationType; +import org.springframework.amqp.core.Declarable; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.util.Assert; + +/** + * Create Super Stream Topology {@link Declarable}s. + * + * @author Gary Russell + * @author Sergei Kurenchuk + * @author Artem Bilan + * + * @since 3.0 + */ +public class SuperStream extends Declarables { + + /** + * Create a Super Stream with the provided parameters. + * @param name the stream name. + * @param partitions the number of partitions. + */ + public SuperStream(String name, int partitions) { + this(name, partitions, Map.of()); + } + + /** + * Create a Super Stream with the provided parameters. + * @param name the stream name. + * @param partitions the number of partitions. + * @param arguments the stream arguments + * @since 3.1 + */ + public SuperStream(String name, int partitions, Map arguments) { + this(name, partitions, (q, i) -> IntStream.range(0, i) + .mapToObj(String::valueOf) + .collect(Collectors.toList()), + arguments + ); + } + + /** + * Create a Super Stream with the provided parameters. + * @param name the stream name. + * @param partitions the number of partitions. + * @param routingKeyStrategy a strategy to determine routing keys to use for the + * partitions. The first parameter is the queue name, the second the number of + * partitions, the returned list must have a size equal to the partitions. + */ + public SuperStream(String name, int partitions, BiFunction> routingKeyStrategy) { + this(name, partitions, routingKeyStrategy, Map.of()); + } + + /** + * Create a Super Stream with the provided parameters. + * @param name the stream name. + * @param partitions the number of partitions. + * @param routingKeyStrategy a strategy to determine routing keys to use for the + * partitions. The first parameter is the queue name, the second the number of + * partitions, the returned list must have a size equal to the partitions. + * @param arguments the stream arguments + * @since 3.1 + */ + public SuperStream(String name, int partitions, BiFunction> routingKeyStrategy, + Map arguments) { + + super(declarables(name, partitions, routingKeyStrategy, arguments)); + } + + private static Collection declarables(String name, int partitions, + BiFunction> routingKeyStrategy, + Map arguments) { + + List declarables = new ArrayList<>(); + List rks = routingKeyStrategy.apply(name, partitions); + Assert.state(rks.size() == partitions, () -> "Expected " + partitions + " routing keys, not " + rks.size()); + declarables.add( + new DirectExchange(name, true, false, Map.of("x-super-stream", true))); + + Map argumentsCopy = new HashMap<>(arguments); + argumentsCopy.put("x-queue-type", "stream"); + for (int i = 0; i < partitions; i++) { + String rk = rks.get(i); + Queue q = new Queue(name + "-" + i, true, false, false, argumentsCopy); + declarables.add(q); + declarables.add(new Binding(q.getName(), DestinationType.QUEUE, name, rk, + Map.of("x-stream-partition-order", i))); + } + return declarables; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java new file mode 100644 index 0000000000..82175b5914 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java @@ -0,0 +1,172 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.StringUtils; + +/** + * Builds a Spring AMQP Super Stream using a fluent API. + * Based on Streams documentation + * + * @author Sergei Kurenchuk + * @author Gary Russell + * @since 3.1 + */ +public class SuperStreamBuilder { + + private final Map arguments = new HashMap<>(); + + private @Nullable String name; + + private int partitions = -1; + + private @Nullable BiFunction> routingKeyStrategy; + + /** + * Creates a builder for Super Stream. + * @param name stream name + * @return the builder + */ + public static SuperStreamBuilder superStream(String name) { + SuperStreamBuilder builder = new SuperStreamBuilder(); + builder.name(name); + return builder; + } + + /** + * Creates a builder for Super Stream. + * @param name stream name + * @param partitions partitions number + * @return the builder + */ + public static SuperStreamBuilder superStream(String name, int partitions) { + return superStream(name).partitions(partitions); + } + + /** + * Set the maximum age retention per stream, which will remove the oldest data. + * @param maxAge valid units: Y, M, D, h, m, s. For example: "7D" for a week + * @return the builder + */ + public SuperStreamBuilder maxAge(String maxAge) { + return withArgument("x-max-age", maxAge); + } + + /** + * Set the maximum log size as the retention configuration for each stream, + * which will truncate the log based on the data size. + * @param bytes the max total size in bytes + * @return the builder + */ + public SuperStreamBuilder maxLength(long bytes) { + return withArgument("max-length-bytes", bytes); + } + + /** + * Set the maximum size limit for segment file. + * @param bytes the max segments size in bytes + * @return the builder + */ + public SuperStreamBuilder maxSegmentSize(long bytes) { + return withArgument("x-stream-max-segment-size-bytes", bytes); + } + + /** + * Set initial replication factor for each partition. + * @param count number of nodes per partition + * @return the builder + */ + public SuperStreamBuilder initialClusterSize(int count) { + return withArgument("x-initial-cluster-size", count); + } + + /** + * Set extra argument which is not covered by builder's methods. + * @param key argument name + * @param value argument value + * @return the builder + */ + public SuperStreamBuilder withArgument(String key, Object value) { + if ("x-queue-type".equals(key) && !"stream".equals(value)) { + throw new IllegalArgumentException("Changing x-queue-type argument is not permitted"); + } + this.arguments.put(key, value); + return this; + } + + /** + * Set the stream name. + * @param name the stream name. + * @return the builder + */ + public SuperStreamBuilder name(String name) { + this.name = name; + return this; + } + + /** + * Set the partitions number. + * @param partitions the partitions number + * @return the builder + */ + public SuperStreamBuilder partitions(int partitions) { + this.partitions = partitions; + return this; + } + + /** + * Set a strategy to determine routing keys to use for the + * partitions. The first parameter is the queue name, the second the number of + * partitions, the returned list must have a size equal to the partitions. + * @param routingKeyStrategy the strategy + * @return the builder + */ + public SuperStreamBuilder routingKeyStrategy(BiFunction> routingKeyStrategy) { + this.routingKeyStrategy = routingKeyStrategy; + return this; + } + + /** + * Builds a final Super Stream. + * @return the Super Stream instance + */ + public SuperStream build() { + if (!StringUtils.hasText(this.name)) { + throw new IllegalArgumentException("Stream name can't be empty"); + } + + if (this.partitions <= 0) { + throw new IllegalArgumentException( + String.format("Partitions number should be great then zero. Current value; %d", this.partitions) + ); + } + + if (this.routingKeyStrategy == null) { + return new SuperStream(this.name, this.partitions, this.arguments); + } + + return new SuperStream(this.name, this.partitions, this.routingKeyStrategy, this.arguments); + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/package-info.java index 7b55932b3d..fb1cebfbc2 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for stream listener configuration. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.config; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/ConsumerCustomizer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/ConsumerCustomizer.java index 501cb3ffe5..f1a7f7d9dc 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/ConsumerCustomizer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/ConsumerCustomizer.java @@ -21,7 +21,9 @@ import com.rabbitmq.stream.ConsumerBuilder; /** - * Customizer for {@link ConsumerBuilder}. + * Customizer for {@link ConsumerBuilder}. The first parameter should be the bean name (or + * listener id) of the component that calls this customizer. Refer to the RabbitMQ Stream + * Java Client for customization options. * * @author Gary Russell * @since 2.4 diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 4063b935fb..563e0ef42e 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,51 +16,85 @@ package org.springframework.rabbit.stream.listener; -import org.apache.commons.logging.Log; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.rabbitmq.stream.Codec; +import com.rabbitmq.stream.Consumer; +import com.rabbitmq.stream.ConsumerBuilder; +import com.rabbitmq.stream.Environment; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.aop.Advice; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.MicrometerHolder; +import org.springframework.amqp.rabbit.listener.ObservableListenerContainer; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; -import org.springframework.beans.factory.BeanNameAware; -import org.springframework.lang.Nullable; +import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.core.log.LogAccessor; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservation; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservation.DefaultRabbitStreamListenerObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamMessageReceiverContext; import org.springframework.rabbit.stream.support.StreamMessageProperties; import org.springframework.rabbit.stream.support.converter.DefaultStreamMessageConverter; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; import org.springframework.util.Assert; -import com.rabbitmq.stream.Codec; -import com.rabbitmq.stream.Consumer; -import com.rabbitmq.stream.ConsumerBuilder; -import com.rabbitmq.stream.Environment; - /** * A listener container for RabbitMQ Streams. * * @author Gary Russell + * @author Christian Tzolov + * @author Ngoc Nhan + * @author Artem Bilan + * @author David Horak + * * @since 2.4 * */ -public class StreamListenerContainer implements MessageListenerContainer, BeanNameAware { +public class StreamListenerContainer extends ObservableListenerContainer { - protected Log logger = LogFactory.getLog(getClass()); // NOSONAR + protected LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); // NOSONAR + + private final Lock lock = new ReentrantLock(); private final ConsumerBuilder builder; - private StreamMessageConverter messageConverter; + private final Collection consumers = new ArrayList<>(); + + private StreamMessageConverter streamConverter; - private ConsumerCustomizer consumerCustomizer = (id, con) -> { }; + private ConsumerCustomizer consumerCustomizer = (id, con) -> { + }; - private Consumer consumer; + private boolean simpleStream; - private String listenerId; + private boolean superStream; - private String beanName; + private int concurrency = 1; private boolean autoStartup = true; - private MessageListener messageListener; + private @Nullable MessageListener messageListener; + + private @Nullable StreamMessageListener streamListener; + + private Advice @Nullable [] adviceChain; + + @SuppressWarnings("NullAway.Init") + private String streamName; + + private @Nullable RabbitStreamListenerObservationConvention observationConvention; /** * Construct an instance using the provided environment. @@ -78,13 +112,76 @@ public StreamListenerContainer(Environment environment) { public StreamListenerContainer(Environment environment, @Nullable Codec codec) { Assert.notNull(environment, "'environment' cannot be null"); this.builder = environment.consumerBuilder(); - this.messageConverter = new DefaultStreamMessageConverter(codec); + this.streamConverter = + codec != null + ? new DefaultStreamMessageConverter(codec) + : new DefaultStreamMessageConverter(); + } + + /** + * Get a stream name this listener is subscribed to. + * @return the stream name this listener is subscribed to. + * @since 3.2.3 + */ + public String getStreamName() { + return this.streamName; } + /** + * {@inheritDoc} + * Mutually exclusive with {@link #superStream(String, String)}. + */ @Override - public void setQueueNames(String... queueNames) { + public void setQueueNames(String @Nullable ... queueNames) { + Assert.isTrue(!this.superStream, "setQueueNames() and superStream() are mutually exclusive"); Assert.isTrue(queueNames != null && queueNames.length == 1, "Only one stream is supported"); - this.builder.stream(queueNames[0]); + this.lock.lock(); + try { + this.builder.stream(queueNames[0]); + this.simpleStream = true; + this.streamName = queueNames[0]; + } + finally { + this.lock.unlock(); + } + } + + /** + * Enable Single Active Consumer on a Super Stream, with one consumer. + * Mutually exclusive with {@link #setQueueNames(String...)}. + * @param streamName the stream. + * @param name the consumer name. + * @since 3.0 + */ + public void superStream(String streamName, String name) { + superStream(streamName, name, 1); + } + + /** + * Enable Single Active Consumer on a Super Stream with the provided number of consumers. + * There must be at least that number of partitions in the Super Stream. + * Mutually exclusive with {@link #setQueueNames(String...)}. + * @param streamName the stream. + * @param name the consumer name. + * @param consumers the number of consumers. + * @since 3.0 + */ + public void superStream(String streamName, String name, int consumers) { + this.lock.lock(); + try { + Assert.isTrue(consumers > 0, () -> "'concurrency' must be greater than zero, not " + consumers); + this.concurrency = consumers; + Assert.isTrue(!this.simpleStream, "setQueueNames() and superStream() are mutually exclusive"); + Assert.notNull(streamName, "'superStream' cannot be null"); + this.builder.superStream(streamName) + .singleActiveConsumer() + .name(name); + this.superStream = true; + this.streamName = streamName; + } + finally { + this.lock.unlock(); + } } /** @@ -93,8 +190,8 @@ public void setQueueNames(String... queueNames) { * {@link org.springframework.amqp.core.Message}. * @return the converter. */ - public StreamMessageConverter getMessageConverter() { - return this.messageConverter; + public StreamMessageConverter getStreamConverter() { + return this.streamConverter; } /** @@ -103,106 +200,215 @@ public StreamMessageConverter getMessageConverter() { * {@link org.springframework.amqp.core.Message}. * @param messageConverter the converter. */ - public void setMessageConverter(StreamMessageConverter messageConverter) { + public void setStreamConverter(StreamMessageConverter messageConverter) { Assert.notNull(messageConverter, "'messageConverter' cannot be null"); - this.messageConverter = messageConverter; + this.streamConverter = messageConverter; } /** * Customize the consumer builder before it is built. * @param consumerCustomizer the customizer. */ - public synchronized void setConsumerCustomizer(ConsumerCustomizer consumerCustomizer) { - Assert.notNull(consumerCustomizer, "'consumerCustomizer' cannot be null"); - this.consumerCustomizer = consumerCustomizer; + public void setConsumerCustomizer(ConsumerCustomizer consumerCustomizer) { + this.lock.lock(); + try { + Assert.notNull(consumerCustomizer, "'consumerCustomizer' cannot be null"); + this.consumerCustomizer = consumerCustomizer; + } + finally { + this.lock.unlock(); + } } - /** - * The 'id' attribute of the listener. - * @return the id (or the container bean name if no id set). - */ - @Nullable - public String getListenerId() { - return this.listenerId != null ? this.listenerId : this.beanName; + @Override + public void setAutoStartup(boolean autoStart) { + this.autoStartup = autoStart; } @Override - public void setListenerId(String listenerId) { - this.listenerId = listenerId; + public boolean isAutoStartup() { + return this.autoStartup; } /** - * Return the bean name. - * @return the bean name. + * Set an advice chain to apply to the listener. + * @param advices the advice chain. + * @since 2.4.5 */ - @Nullable - public String getBeanName() { - return this.beanName; + public void setAdviceChain(Advice... advices) { + Assert.notNull(advices, "'advices' cannot be null"); + Assert.noNullElements(advices, "'advices' cannot have null elements"); + this.adviceChain = Arrays.copyOf(advices, advices.length); } @Override - public void setBeanName(String beanName) { - this.beanName = beanName; + public @Nullable Object getMessageListener() { + return this.messageListener; } - @Override - public void setAutoStartup(boolean autoStart) { - this.autoStartup = autoStart; + /** + * Set a RabbitStreamListenerObservationConvention; used to add additional key/values + * to observations when using a {@link StreamMessageListener}. + * @param observationConvention the convention. + * @since 3.0.5 + */ + public void setObservationConvention(RabbitStreamListenerObservationConvention observationConvention) { + this.observationConvention = observationConvention; } @Override - public boolean isAutoStartup() { - return this.autoStartup; - } - @Override - @Nullable - public Object getMessageListener() { - return this.messageListener; + public void afterPropertiesSet() { + checkMicrometer(); + checkObservation(); } @Override - public synchronized boolean isRunning() { - return this.consumer != null; + public boolean isRunning() { + this.lock.lock(); + try { + return !this.consumers.isEmpty(); + } + finally { + this.lock.unlock(); + } } @Override - public synchronized void start() { - if (this.consumer == null) { - this.consumerCustomizer.accept(getListenerId(), this.builder); - this.consumer = this.builder.build(); + public void start() { + this.lock.lock(); + try { + if (this.consumers.isEmpty()) { + this.consumerCustomizer.accept(getListenerId(), this.builder); + if (this.simpleStream) { + this.consumers.add(this.builder.build()); + } + else { + for (int i = 0; i < this.concurrency; i++) { + this.consumers.add(this.builder.build()); + } + } + } + } + finally { + this.lock.unlock(); } } @Override - public synchronized void stop() { - if (this.consumer != null) { - this.consumer.close(); - this.consumer = null; + public void stop() { + this.lock.lock(); + try { + this.consumers.forEach(consumer -> { + try { + consumer.close(); + } + catch (RuntimeException ex) { + this.logger.error(ex, "Failed to close consumer"); + } + }); + this.consumers.clear(); + } + finally { + this.lock.unlock(); } } @Override public void setupMessageListener(MessageListener messageListener) { - this.messageListener = messageListener; + adviseIfNeeded(messageListener); this.builder.messageHandler((context, message) -> { - if (messageListener instanceof StreamMessageListener) { - ((StreamMessageListener) messageListener).onStreamMessage(message, context); + ObservationRegistry registry = getObservationRegistry(); + Object sample = null; + MicrometerHolder micrometerHolder = getMicrometerHolder(); + if (micrometerHolder != null) { + sample = micrometerHolder.start(); + } + Observation observation = + RabbitStreamListenerObservation.STREAM_LISTENER_OBSERVATION.observation(this.observationConvention, + DefaultRabbitStreamListenerObservationConvention.INSTANCE, + () -> new RabbitStreamMessageReceiverContext(message, getListenerId(), this.streamName), + registry); + Object finalSample = sample; + StreamMessageListener streamListenerToUse = this.streamListener; + if (streamListenerToUse != null) { + observation.observe(() -> { + try { + streamListenerToUse.onStreamMessage(message, context); + if (micrometerHolder != null && finalSample != null) { + micrometerHolder.success(finalSample, this.streamName); + } + } + catch (RuntimeException rtex) { + if (micrometerHolder != null && finalSample != null) { + micrometerHolder.failure(finalSample, this.streamName, rtex.getClass().getSimpleName()); + } + throw rtex; + } + catch (Exception ex) { + if (micrometerHolder != null && finalSample != null) { + micrometerHolder.failure(finalSample, this.streamName, ex.getClass().getSimpleName()); + } + throw RabbitExceptionTranslator.convertRabbitAccessException(ex); + } + }); } else { - Message message2 = this.messageConverter.toMessage(message, new StreamMessageProperties(context)); - if (messageListener instanceof ChannelAwareMessageListener) { + Message message2 = this.streamConverter.toMessage(message, new StreamMessageProperties(context)); + if (this.messageListener instanceof ChannelAwareMessageListener channelAwareMessageListener) { try { - ((ChannelAwareMessageListener) messageListener).onMessage(message2, null); + observation.observe(() -> { + try { + channelAwareMessageListener.onMessage(message2, null); + if (micrometerHolder != null && finalSample != null) { + micrometerHolder.success(finalSample, this.streamName); + } + } + catch (RuntimeException rtex) { + if (micrometerHolder != null && finalSample != null) { + micrometerHolder.failure(finalSample, this.streamName, + rtex.getClass().getSimpleName()); + } + throw rtex; + } + catch (Exception ex) { + if (micrometerHolder != null && finalSample != null) { + micrometerHolder.failure(finalSample, this.streamName, ex.getClass().getSimpleName()); + } + throw RabbitExceptionTranslator.convertRabbitAccessException(ex); + } + }); } - catch (Exception e) { // NOSONAR - this.logger.error("Listner threw an exception", e); + catch (Exception ex) { // NOSONAR + this.logger.error(ex, "Listener threw an exception"); } } else { - messageListener.onMessage(message2); + MessageListener messageListenerToUse = this.messageListener; + Assert.state(messageListenerToUse != null, "'messageListener' or 'streamListener' is required"); + observation.observe(() -> messageListenerToUse.onMessage(message2)); } } }); } + private void adviseIfNeeded(MessageListener messageListener) { + this.messageListener = messageListener; + if (messageListener instanceof StreamMessageListener streamMessageListener) { + this.streamListener = streamMessageListener; + } + if (this.adviceChain != null && this.adviceChain.length > 0) { + ProxyFactory factory = new ProxyFactory(messageListener); + for (Advice advice : this.adviceChain) { + factory.addAdvisor(new DefaultPointcutAdvisor(advice)); + } + factory.setInterfaces(messageListener.getClass().getInterfaces()); + if (this.streamListener != null) { + this.streamListener = (StreamMessageListener) factory.getProxy(getClass().getClassLoader()); + } + else { + this.messageListener = (MessageListener) factory.getProxy(getClass().getClassLoader()); + } + } + } + } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamMessageListener.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamMessageListener.java index 679f6525f2..738b08cb62 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamMessageListener.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package org.springframework.rabbit.stream.listener; -import org.springframework.amqp.core.MessageListener; - import com.rabbitmq.stream.Message; import com.rabbitmq.stream.MessageHandler.Context; +import org.springframework.amqp.core.MessageListener; + /** * A message listener that receives native stream messages. * diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java index 3db6b3a3fe..870d5d3050 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author 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,14 +18,17 @@ import java.lang.reflect.Method; +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.MessageHandler.Context; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.listener.adapter.InvocationResult; import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; +import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import org.springframework.messaging.support.GenericMessage; import org.springframework.rabbit.stream.listener.StreamMessageListener; -import com.rabbitmq.stream.Message; -import com.rabbitmq.stream.MessageHandler.Context; - /** * A listener adapter that receives native stream messages. * @@ -35,6 +38,11 @@ */ public class StreamMessageListenerAdapter extends MessagingMessageListenerAdapter implements StreamMessageListener { + /** + * The {@code org.springframework.messaging.handler.invocation.InvocableHandlerMethod} contact support. + */ + private static final GenericMessage FAKE_MESSAGE = new GenericMessage<>(""); + /** * Construct an instance with the provided arguments. * @param bean the bean. @@ -42,8 +50,8 @@ public class StreamMessageListenerAdapter extends MessagingMessageListenerAdapte * @param returnExceptions true to return exceptions. * @param errorHandler the error handler. */ - public StreamMessageListenerAdapter(Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler) { + public StreamMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler) { super(bean, method, returnExceptions, errorHandler); } @@ -51,7 +59,7 @@ public StreamMessageListenerAdapter(Object bean, Method method, boolean returnEx @Override public void onStreamMessage(Message message, Context context) { try { - InvocationResult result = getHandlerAdapter().invoke(null, message, context); + InvocationResult result = getHandlerAdapter().invoke(FAKE_MESSAGE, message, context); if (result.getReturnValue() != null) { logger.warn("Replies are not currently supported with native Stream listeners"); } @@ -60,7 +68,7 @@ public void onStreamMessage(Message message, Context context) { } } catch (Exception ex) { - this.logger.error("Failed to invoke listener", ex); + throw new ListenerExecutionFailedException("Failed to invoke listener", ex); } } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/package-info.java index 80f24d55c5..cc90755ae2 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for stream listener adapters. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.listener.adapter; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/package-info.java index 64517ce8c3..ff1d7c28a7 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for stream listeners. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.listener; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservation.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservation.java new file mode 100644 index 0000000000..cc12130fdf --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservation.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.micrometer; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * Spring Rabbit Observation for stream listeners. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public enum RabbitStreamListenerObservation implements ObservationDocumentation { + + /** + * Observation for Rabbit stream listeners. + */ + STREAM_LISTENER_OBSERVATION { + + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitStreamListenerObservationConvention.class; + } + + @Override + public String getPrefix() { + return "spring.rabbit.stream.listener"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ListenerLowCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum ListenerLowCardinalityTags implements KeyName { + + /** + * Listener id. + */ + LISTENER_ID { + + @Override + public String asString() { + return "spring.rabbit.stream.listener.id"; + } + + } + + } + + /** + * Default {@link RabbitStreamListenerObservationConvention} for Rabbit listener key values. + */ + public static class DefaultRabbitStreamListenerObservationConvention + implements RabbitStreamListenerObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitStreamListenerObservationConvention INSTANCE = + new DefaultRabbitStreamListenerObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitStreamMessageReceiverContext context) { + return KeyValues.of(RabbitStreamListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), + context.getListenerId()); + } + + @Override + public String getContextualName(RabbitStreamMessageReceiverContext context) { + return context.getSource() + " receive"; + } + + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservationConvention.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservationConvention.java new file mode 100644 index 0000000000..ffa90c5d23 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservationConvention.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit stream listener key values. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public interface RabbitStreamListenerObservationConvention + extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitStreamMessageReceiverContext; + } + + @Override + default String getName() { + return "spring.rabbit.stream.listener"; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java new file mode 100644 index 0000000000..020fbba8df --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java @@ -0,0 +1,100 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.micrometer; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import com.rabbitmq.stream.Message; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.transport.ReceiverContext; + +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageReceiverContext; + +/** + * {@link ReceiverContext} for stream {@link Message}s. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public class RabbitStreamMessageReceiverContext extends ReceiverContext { + + private final String listenerId; + + private final String stream; + + @SuppressWarnings("this-escape") + public RabbitStreamMessageReceiverContext(Message message, String listenerId, String stream) { + super((carrier, key) -> { + Map props = carrier.getApplicationProperties(); + if (props != null) { + Object value = carrier.getApplicationProperties().get(key); + if (value instanceof String string) { + return string; + } + else if (value instanceof byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } + } + return null; + }); + setCarrier(message); + this.listenerId = listenerId; + this.stream = stream; + setRemoteServiceName("RabbitMQ Stream"); + } + + public String getListenerId() { + return this.listenerId; + } + + /** + * Return the source (stream) for this message. + * @return the source. + */ + public String getSource() { + return this.stream; + } + + /** + * Default {@link RabbitListenerObservationConvention} for Rabbit listener key values. + */ + public static class DefaultRabbitListenerObservationConvention implements RabbitListenerObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitListenerObservationConvention INSTANCE = + new DefaultRabbitListenerObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { + return KeyValues.of(RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), + context.getListenerId()); + } + + @Override + public String getContextualName(RabbitMessageReceiverContext context) { + return context.getSource() + " receive"; + } + + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java new file mode 100644 index 0000000000..b2ee9819b6 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.micrometer; + +import java.util.Map; + +import com.rabbitmq.stream.Message; +import io.micrometer.observation.transport.SenderContext; + +/** + * {@link SenderContext} for {@link Message}s. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public class RabbitStreamMessageSenderContext extends SenderContext { + + private final String beanName; + + private final String destination; + + @SuppressWarnings("this-escape") + public RabbitStreamMessageSenderContext(Message message, String beanName, String destination) { + super((carrier, key, value) -> { + Map props = message.getApplicationProperties(); + if (props != null) { + props.put(key, value); + } + }); + setCarrier(message); + this.beanName = beanName; + this.destination = destination; + setRemoteServiceName("RabbitMQ Stream"); + } + + public String getBeanName() { + return this.beanName; + } + + /** + * Return the destination - {@code exchange/routingKey}. + * @return the destination. + */ + public String getDestination() { + return this.destination; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java new file mode 100644 index 0000000000..777031ce48 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java @@ -0,0 +1,103 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.micrometer; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; + +/** + * Spring RabbitMQ Observation for + * {@link org.springframework.rabbit.stream.producer.RabbitStreamTemplate}. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public enum RabbitStreamTemplateObservation implements ObservationDocumentation { + + /** + * Observation for {@link RabbitStreamTemplate}s. + */ + STREAM_TEMPLATE_OBSERVATION { + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitStreamTemplateObservationConvention.class; + } + + @Override + public String getPrefix() { + return "spring.rabbit.stream.template"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return TemplateLowCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum TemplateLowCardinalityTags implements KeyName { + + /** + * Bean name of the template. + */ + BEAN_NAME { + + @Override + public String asString() { + return "spring.rabbit.stream.template.name"; + } + + } + + } + + /** + * Default {@link RabbitStreamTemplateObservationConvention} for Rabbit template key values. + */ + public static class DefaultRabbitStreamTemplateObservationConvention + implements RabbitStreamTemplateObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitStreamTemplateObservationConvention INSTANCE = + new DefaultRabbitStreamTemplateObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitStreamMessageSenderContext context) { + return KeyValues.of(RabbitStreamTemplateObservation.TemplateLowCardinalityTags.BEAN_NAME.asString(), + context.getBeanName()); + } + + @Override + public String getContextualName(RabbitStreamMessageSenderContext context) { + return context.getDestination() + " send"; + } + + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservationConvention.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservationConvention.java new file mode 100644 index 0000000000..1989bd079e --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservationConvention.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit stream template key values. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public interface RabbitStreamTemplateObservationConvention + extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitStreamMessageSenderContext; + } + + @Override + default String getName() { + return "spring.rabbit.stream.template"; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java new file mode 100644 index 0000000000..28a009bc25 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides classes for Micrometer support. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.rabbit.stream.micrometer; diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/ProducerCustomizer.java similarity index 51% rename from spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java rename to spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/ProducerCustomizer.java index 9aac7226cc..e615d85d1c 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/ProducerCustomizer.java @@ -14,32 +14,21 @@ * limitations under the License. */ -package org.springframework.rabbit.stream.listener; +package org.springframework.rabbit.stream.producer; -import java.time.Duration; +import java.util.function.BiConsumer; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.utility.DockerImageName; +import com.rabbitmq.stream.ProducerBuilder; /** + * Called to enable customization of the {@link ProducerBuilder} when a new producer is + * created. The first parameter should be the bean name of the component that calls this + * customizer. Refer to the RabbitMQ Stream Java Client for customization options. + * * @author Gary Russell * @since 2.4 * */ -public abstract class AbstractIntegrationTests { - - static final GenericContainer RABBITMQ; - - static { - String image = "pivotalrabbitmq/rabbitmq-stream"; - String cache = System.getenv().get("IMAGE_CACHE"); - if (cache != null) { - image = cache + image; - } - RABBITMQ = new GenericContainer<>(DockerImageName.parse(image)) - .withExposedPorts(5672, 15672, 5552) - .withStartupTimeout(Duration.ofMinutes(2)); - RABBITMQ.start(); - } - +@FunctionalInterface +public interface ProducerCustomizer extends BiConsumer { } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java new file mode 100644 index 0000000000..f4ffe4f6c6 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.producer; + +import java.util.concurrent.CompletableFuture; + +import com.rabbitmq.stream.MessageBuilder; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpException; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; + +/** + * Provides methods for sending messages using a RabbitMQ Stream producer. + * + * @author Gary Russell + * @since 2.4 + * + */ +public interface RabbitStreamOperations extends AutoCloseable { + + /** + * Send a Spring AMQP message. + * @param message the message. + * @return a future to indicate success/failure. + */ + CompletableFuture send(Message message); + + /** + * Convert to and send a Spring AMQP message. + * @param message the payload. + * @return a future to indicate success/failure. + */ + CompletableFuture convertAndSend(Object message); + + /** + * Convert to and send a Spring AMQP message. If a {@link MessagePostProcessor} is + * provided and returns {@code null}, the message is not sent and the future is + * completed with {@code false}. + * @param message the payload. + * @param mpp a message post processor. + * @return a future to indicate success/failure. + */ + CompletableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); + + /** + * Send a native stream message. + * @param message the message. + * @return a future to indicate success/failure. + * @see #messageBuilder() + */ + CompletableFuture send(com.rabbitmq.stream.Message message); + + /** + * Return the producer's {@link MessageBuilder} to create native stream messages. + * @return the builder. + * @see #send(com.rabbitmq.stream.Message) + */ + MessageBuilder messageBuilder(); + + /** + * Return the message converter. + * @return the converter. + */ + MessageConverter messageConverter(); + + /** + * Return the stream message converter. + * @return the converter; + */ + StreamMessageConverter streamMessageConverter(); + + @Override + default void close() throws AmqpException { + // narrow exception to avoid compiler warning - see + // https://bugs.openjdk.java.net/browse/JDK-8155591 + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java new file mode 100644 index 0000000000..f816da8ac5 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -0,0 +1,345 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.producer; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; + +import com.rabbitmq.stream.ConfirmationHandler; +import com.rabbitmq.stream.Constants; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.MessageBuilder; +import com.rabbitmq.stream.Producer; +import com.rabbitmq.stream.ProducerBuilder; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.log.LogAccessor; +import org.springframework.rabbit.stream.micrometer.RabbitStreamMessageSenderContext; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservation; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservation.DefaultRabbitStreamTemplateObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservationConvention; +import org.springframework.rabbit.stream.support.StreamMessageProperties; +import org.springframework.rabbit.stream.support.converter.DefaultStreamMessageConverter; +import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link RabbitStreamOperations}. + * + * @author Gary Russell + * @author Christian Tzolov + * @author Ngoc Nhan + * @since 2.4 + * + */ +public class RabbitStreamTemplate implements RabbitStreamOperations, ApplicationContextAware, BeanNameAware { + + protected final LogAccessor logger = new LogAccessor(getClass()); // NOSONAR + + private final Lock lock = new ReentrantLock(); + + @SuppressWarnings("NullAway.Init") + private ApplicationContext applicationContext; + + private final Environment environment; + + private final String streamName; + + private @Nullable Function superStreamRouting; + + private MessageConverter messageConverter = new SimpleMessageConverter(); + + private StreamMessageConverter streamConverter = new DefaultStreamMessageConverter(); + + private boolean streamConverterSet; + + @SuppressWarnings("NullAway.Init") + private String beanName; + + private ProducerCustomizer producerCustomizer = (name, builder) -> { }; + + private boolean observationEnabled; + + private @Nullable RabbitStreamTemplateObservationConvention observationConvention; + + private @Nullable ObservationRegistry observationRegistry; + + private volatile @Nullable Producer producer; + + private volatile boolean observationRegistryObtained; + + /** + * Construct an instance with the provided {@link Environment}. + * @param environment the environment. + * @param streamName the stream name. + */ + public RabbitStreamTemplate(Environment environment, String streamName) { + Assert.notNull(environment, "'environment' cannot be null"); + Assert.notNull(streamName, "'streamName' cannot be null"); + this.environment = environment; + this.streamName = streamName; + } + + + private Producer createOrGetProducer() { + Producer producerToUse = this.producer; + if (producerToUse == null) { + this.lock.lock(); + try { + producerToUse = this.producer; + if (producerToUse == null) { + ProducerBuilder builder = this.environment.producerBuilder(); + if (this.superStreamRouting == null) { + builder.stream(this.streamName); + } + else { + builder.superStream(this.streamName) + .routing(this.superStreamRouting); + } + this.producerCustomizer.accept(this.beanName, builder); + producerToUse = builder.build(); + this.producer = producerToUse; + if (!this.streamConverterSet) { + ((DefaultStreamMessageConverter) this.streamConverter) + .setBuilderSupplier(producerToUse::messageBuilder); + } + } + } + finally { + this.lock.unlock(); + } + } + return producerToUse; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void setBeanName(String name) { + this.lock.lock(); + try { + this.beanName = name; + } + finally { + this.lock.unlock(); + } + } + + /** + * Add a routing function, making the stream a super stream. + * @param superStreamRouting the routing function. + * @since 3.0 + */ + public void setSuperStreamRouting(Function superStreamRouting) { + this.lock.lock(); + try { + this.superStreamRouting = superStreamRouting; + } + finally { + this.lock.unlock(); + } + } + + + /** + * Set a converter for {@link #convertAndSend(Object)} operations. + * @param messageConverter the converter. + */ + public void setMessageConverter(MessageConverter messageConverter) { + Assert.notNull(messageConverter, "'messageConverter' cannot be null"); + this.messageConverter = messageConverter; + } + + /** + * Set a converter to convert from {@link Message} to {@link com.rabbitmq.stream.Message} + * for {@link #send(Message)} and {@link #convertAndSend(Object)} methods. + * @param streamConverter the converter. + */ + public void setStreamConverter(StreamMessageConverter streamConverter) { + Assert.notNull(streamConverter, "'streamConverter' cannot be null"); + this.lock.lock(); + try { + this.streamConverter = streamConverter; + this.streamConverterSet = true; + } + finally { + this.lock.unlock(); + } + } + + /** + * Used to customize the {@link ProducerBuilder} before the {@link Producer} is built. + * @param producerCustomizer the customizer; + */ + public void setProducerCustomizer(ProducerCustomizer producerCustomizer) { + Assert.notNull(producerCustomizer, "'producerCustomizer' cannot be null"); + this.lock.lock(); + try { + this.producerCustomizer = producerCustomizer; + } + finally { + this.lock.unlock(); + } + } + + /** + * Set to true to enable Micrometer observation. + * @param observationEnabled true to enable. + * @since 3.0.5 + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + + @Override + public MessageConverter messageConverter() { + return this.messageConverter; + } + + + @Override + public StreamMessageConverter streamMessageConverter() { + return this.streamConverter; + } + + + @Override + public CompletableFuture send(Message message) { + CompletableFuture future = new CompletableFuture<>(); + observeSend(this.streamConverter.fromMessage(message), future); + return future; + } + + @Override + public CompletableFuture convertAndSend(Object message) { + return convertAndSend(message, null); + } + + @Override + public CompletableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp) { + Message message2 = this.messageConverter.toMessage(message, new StreamMessageProperties()); + if (mpp != null) { + message2 = mpp.postProcessMessage(message2); + } + return send(message2); + } + + + @Override + public CompletableFuture send(com.rabbitmq.stream.Message message) { + CompletableFuture future = new CompletableFuture<>(); + observeSend(message, future); + return future; + } + + @SuppressWarnings({ "NullAway", "try" }) // Dataflow analysis limitation + private void observeSend(com.rabbitmq.stream.Message message, CompletableFuture future) { + Observation observation = RabbitStreamTemplateObservation.STREAM_TEMPLATE_OBSERVATION.observation( + this.observationConvention, DefaultRabbitStreamTemplateObservationConvention.INSTANCE, + () -> new RabbitStreamMessageSenderContext(message, this.beanName, this.streamName), + obtainObservationRegistry()); + observation.start(); + try { + createOrGetProducer().send(message, handleConfirm(future, observation)); + } + catch (Exception ex) { + observation.error(ex); + observation.stop(); + future.completeExceptionally(ex); + } + } + + private @Nullable ObservationRegistry obtainObservationRegistry() { + if (!this.observationRegistryObtained && this.observationEnabled) { + ObjectProvider registry = + this.applicationContext.getBeanProvider(ObservationRegistry.class); + this.observationRegistry = registry.getIfUnique(); + this.observationRegistryObtained = true; + } + return this.observationRegistry; + } + + @Override + @SuppressWarnings("try") + public MessageBuilder messageBuilder() { + return createOrGetProducer().messageBuilder(); + } + + private ConfirmationHandler handleConfirm(CompletableFuture future, Observation observation) { + return confStatus -> { + if (confStatus.isConfirmed()) { + future.complete(true); + observation.stop(); + } + else { + int code = confStatus.getCode(); + String errorMessage = switch (code) { + case Constants.CODE_MESSAGE_ENQUEUEING_FAILED -> "Message Enqueueing Failed"; + case Constants.CODE_PRODUCER_CLOSED -> "Producer Closed"; + case Constants.CODE_PRODUCER_NOT_AVAILABLE -> "Producer Not Available"; + case Constants.CODE_PUBLISH_CONFIRM_TIMEOUT -> "Publish Confirm Timeout"; + default -> "Unknown code: " + code; + }; + StreamSendException ex = new StreamSendException(errorMessage, code); + observation.error(ex); + observation.stop(); + future.completeExceptionally(ex); + } + }; + } + + /** + * {@inheritDoc} + *

+ * Close the underlying producer; a new producer will be created on the next + * operation that requires one. + */ + @Override + public void close() { + if (this.producer != null) { + this.lock.lock(); + try { + Producer producerToCheck = this.producer; + if (producerToCheck != null) { + producerToCheck.close(); + this.producer = null; + } + } + finally { + this.lock.unlock(); + } + } + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java new file mode 100644 index 0000000000..49f03ad2b8 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.producer; + +import java.io.Serial; + +import org.springframework.amqp.AmqpException; + +/** + * Used to complete the future exceptionally when sending fails. + * + * @author Gary Russell + * @since 2.4 + * + */ +public class StreamSendException extends AmqpException { + + @Serial + private static final long serialVersionUID = 1L; + + private final int confirmationCode; + /** + * Construct an instance with the provided message. + * @param message the message. + * @param code the confirmation code. + */ + public StreamSendException(String message, int code) { + super(message); + this.confirmationCode = code; + } + + /** + * Return the confirmation code, if available. + * @return the code. + */ + public int getConfirmationCode() { + return this.confirmationCode; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java new file mode 100644 index 0000000000..553b453556 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides classes for stream producers. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.rabbit.stream.producer; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java new file mode 100644 index 0000000000..bc890b548f --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.retry; + +import com.rabbitmq.stream.MessageHandler.Context; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.retry.MessageRecoverer; + +/** + * Implementations of this interface can handle failed messages after retries are + * exhausted. + * + * @author Gary Russell + * @since 2.4.5 + * + */ +@FunctionalInterface +public interface StreamMessageRecoverer extends MessageRecoverer { + + @Override + default void recover(Message message, Throwable cause) { + } + + /** + * Callback for message that was consumed but failed all retry attempts. + * + * @param message the message to recover. + * @param context the context. + * @param cause the cause of the error. + */ + void recover(com.rabbitmq.stream.Message message, Context context, Throwable cause); + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java new file mode 100644 index 0000000000..3662cf04a4 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.retry; + +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.MessageHandler.Context; + +import org.springframework.amqp.rabbit.config.StatelessRetryOperationsInterceptorFactoryBean; +import org.springframework.amqp.rabbit.retry.MessageRecoverer; +import org.springframework.rabbit.stream.listener.StreamListenerContainer; +import org.springframework.retry.RetryOperations; +import org.springframework.retry.interceptor.MethodInvocationRecoverer; +import org.springframework.retry.support.RetryTemplate; + +/** + * Convenient factory bean for creating a stateless retry interceptor for use in a + * {@link StreamListenerContainer} when consuming native stream messages, giving you a + * large amount of control over the behavior of a container when a listener fails. To + * control the number of retry attempt or the backoff in between attempts, supply a + * customized {@link RetryTemplate}. Stateless retry is appropriate if your listener can + * be called repeatedly between failures with no side effects. The semantics of stateless + * retry mean that a listener exception is not propagated to the container until the retry + * attempts are exhausted. When the retry attempts are exhausted it can be processed using + * a {@link StreamMessageRecoverer} if one is provided. + * + * @author Gary Russell + * + * @see RetryOperations#execute(org.springframework.retry.RetryCallback,org.springframework.retry.RecoveryCallback) + */ +public class StreamRetryOperationsInterceptorFactoryBean extends StatelessRetryOperationsInterceptorFactoryBean { + + @Override + protected MethodInvocationRecoverer createRecoverer() { + return (args, cause) -> { + StreamMessageRecoverer messageRecoverer = (StreamMessageRecoverer) getMessageRecoverer(); + Object arg = args[0]; + if (arg instanceof org.springframework.amqp.core.Message) { + return super.recover(args, cause); + } + else { + if (messageRecoverer == null) { + this.logger.warn("Message(s) dropped on recovery: " + arg, cause); + } + else { + messageRecoverer.recover((Message) arg, (Context) args[1], cause); + } + return null; + } + }; + } + + /** + * Set a {@link StreamMessageRecoverer} to call when retries are exhausted. + * @param messageRecoverer the recoverer. + */ + public void setStreamMessageRecoverer(StreamMessageRecoverer messageRecoverer) { + super.setMessageRecoverer(messageRecoverer); + } + + @Override + public void setMessageRecoverer(MessageRecoverer messageRecoverer) { + throw new UnsupportedOperationException("Use setStreamMessageRecoverer() instead"); + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java new file mode 100644 index 0000000000..98aa4fd99d --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides classes supporting retries. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.rabbit.stream.retry; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java new file mode 100644 index 0000000000..4d9211551c --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java @@ -0,0 +1,100 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.support; + +import java.util.function.Consumer; + +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.StreamCreator; + +import org.springframework.context.SmartLifecycle; +import org.springframework.util.Assert; + +/** + * Used to provision streams. + * + * @author Gary Russell + * @since 2.4.13 + * + */ +public class StreamAdmin implements SmartLifecycle { + + private final StreamCreator streamCreator; + + private final Consumer callback; + + private boolean autoStartup = true; + + private int phase; + + private volatile boolean running; + + /** + * Construct with the provided parameters. + * @param env the environment. + * @param callback the callback to receive the {@link StreamCreator}. + */ + public StreamAdmin(Environment env, Consumer callback) { + Assert.notNull(env, "Environment cannot be null"); + Assert.notNull(callback, "'callback' cannot be null"); + this.streamCreator = env.streamCreator(); + this.callback = callback; + } + + @Override + public int getPhase() { + return this.phase; + } + + /** + * Set the phase; default is 0. + * @param phase the phase. + */ + public void setPhase(int phase) { + this.phase = phase; + } + + /** + * Set to false to prevent automatic startup. + * @param autoStartup the autoStartup. + */ + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + @Override + public void start() { + this.callback.accept(this.streamCreator); + this.running = true; + } + + @Override + public void stop() { + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java index a9d6281c72..1e8c5b5357 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,13 @@ package org.springframework.rabbit.stream.support; +import java.io.Serial; import java.util.Objects; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; - import com.rabbitmq.stream.MessageHandler.Context; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.MessageProperties; /** * {@link MessageProperties} extension for stream messages. @@ -32,21 +33,28 @@ */ public class StreamMessageProperties extends MessageProperties { + @Serial private static final long serialVersionUID = 1L; - private transient Context context; + private transient @Nullable Context context; - private String to; + private @Nullable String to; - private String subject; + private @Nullable String subject; private long creationTime; - private String groupId; + private @Nullable String groupId; private long groupSequence; - private String replyToGroupId; + private @Nullable String replyToGroupId; + + /** + * Create a new instance. + */ + public StreamMessageProperties() { + } /** * Create a new instance with the provided context. @@ -60,8 +68,7 @@ public StreamMessageProperties(@Nullable Context context) { * Return the stream {@link Context} for the message. * @return the context. */ - @Nullable - public Context getContext() { + public @Nullable Context getContext() { return this.context; } @@ -69,7 +76,7 @@ public Context getContext() { * See {@link com.rabbitmq.stream.Properties#getTo()}. * @return the to address. */ - public String getTo() { + public @Nullable String getTo() { return this.to; } @@ -85,7 +92,7 @@ public void setTo(String address) { * See {@link com.rabbitmq.stream.Properties#getSubject()}. * @return the subject. */ - public String getSubject() { + public @Nullable String getSubject() { return this.subject; } @@ -118,7 +125,7 @@ public void setCreationTime(long creationTime) { * See {@link com.rabbitmq.stream.Properties#getGroupId()}. * @return the group id. */ - public String getGroupId() { + public @Nullable String getGroupId() { return this.groupId; } @@ -151,7 +158,7 @@ public void setGroupSequence(long groupSequence) { * See {@link com.rabbitmq.stream.Properties#getReplyToGroupId()}. * @return the reply to group id. */ - public String getReplyToGroupId() { + public @Nullable String getReplyToGroupId() { return this.replyToGroupId; } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java index 9f68c6e238..00a72cdb98 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author 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,6 @@ import java.util.UUID; import java.util.function.Supplier; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.support.converter.MessageConversionException; -import org.springframework.amqp.utils.JavaUtils; -import org.springframework.lang.Nullable; -import org.springframework.rabbit.stream.support.StreamMessageProperties; -import org.springframework.util.Assert; - import com.rabbitmq.stream.Codec; import com.rabbitmq.stream.MessageBuilder; import com.rabbitmq.stream.MessageBuilder.ApplicationPropertiesBuilder; @@ -37,32 +29,48 @@ import com.rabbitmq.stream.Properties; import com.rabbitmq.stream.codec.WrapperMessageBuilder; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.utils.JavaUtils; +import org.springframework.rabbit.stream.support.StreamMessageProperties; +import org.springframework.util.Assert; + /** * Default {@link StreamMessageConverter}. * * @author Gary Russell + * @author Ngoc Nhan * @since 2.4 * */ public class DefaultStreamMessageConverter implements StreamMessageConverter { - private final Supplier builderSupplier; - private final Charset charset = StandardCharsets.UTF_8; + private Supplier builderSupplier; + /** * Construct an instance using a {@link WrapperMessageBuilder}. */ public DefaultStreamMessageConverter() { - this.builderSupplier = () -> new WrapperMessageBuilder(); + this.builderSupplier = WrapperMessageBuilder::new; } /** * Construct an instance using the provided codec. * @param codec the codec. */ - public DefaultStreamMessageConverter(@Nullable Codec codec) { - this.builderSupplier = () -> codec.messageBuilder(); + public DefaultStreamMessageConverter(Codec codec) { + this.builderSupplier = codec::messageBuilder; + } + + /** + * Set a supplier for a message builder. + * @param builderSupplier the supplier. + */ + public void setBuilderSupplier(Supplier builderSupplier) { + this.builderSupplier = builderSupplier; } @Override @@ -96,8 +104,8 @@ public com.rabbitmq.stream.Message fromMessage(Message message) throws MessageCo .acceptIfNotNull(mProps.getGroupId(), propsBuilder::groupId) .acceptIfNotNull(mProps.getGroupSequence(), propsBuilder::groupSequence) .acceptIfNotNull(mProps.getReplyToGroupId(), propsBuilder::replyToGroupId); - if (mProps.getHeaders().size() > 0) { - ApplicationPropertiesBuilder appPropsBuilder = builder.applicationProperties(); + ApplicationPropertiesBuilder appPropsBuilder = builder.applicationProperties(); + if (!mProps.getHeaders().isEmpty()) { mProps.getHeaders().forEach((key, val) -> { mapProp(key, val, appPropsBuilder); }); @@ -107,35 +115,35 @@ public com.rabbitmq.stream.Message fromMessage(Message message) throws MessageCo } private void mapProp(String key, Object val, ApplicationPropertiesBuilder builder) { // NOSONAR - complexity - if (val instanceof String) { - builder.entry(key, (String) val); + if (val instanceof String string) { + builder.entry(key, string); } - else if (val instanceof Long) { - builder.entry(key, (Long) val); + else if (val instanceof Long longValue) { + builder.entry(key, longValue); } - else if (val instanceof Integer) { - builder.entry(key, (Integer) val); + else if (val instanceof Integer intValue) { + builder.entry(key, intValue); } - else if (val instanceof Short) { - builder.entry(key, (Short) val); + else if (val instanceof Short shortValue) { + builder.entry(key, shortValue); } - else if (val instanceof Byte) { - builder.entry(key, (Byte) val); + else if (val instanceof Byte byteValue) { + builder.entry(key, byteValue); } - else if (val instanceof Double) { - builder.entry(key, (Double) val); + else if (val instanceof Double doubleValue) { + builder.entry(key, doubleValue); } - else if (val instanceof Float) { - builder.entry(key, (Float) val); + else if (val instanceof Float floatValue) { + builder.entry(key, floatValue); } - else if (val instanceof Character) { - builder.entry(key, (Character) val); + else if (val instanceof Character character) { + builder.entry(key, character); } - else if (val instanceof UUID) { - builder.entry(key, (UUID) val); + else if (val instanceof UUID uuid) { + builder.entry(key, uuid); } - else if (val instanceof byte[]) { - builder.entry(key, (byte[]) val); + else if (val instanceof byte[] bytes) { + builder.entry(key, bytes); } } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/package-info.java index 5ed17935fc..aa6dca80bf 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for message conversion. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.support.converter; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java new file mode 100644 index 0000000000..a8720b68b4 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides support classes. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.rabbit.stream.support; diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java new file mode 100644 index 0000000000..bb0fd60180 --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2021-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.config; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.Queue; + +/** + * @author Sergei Kurenchuk + * @since 3.1 + */ +public class SuperStreamConfigurationTests { + + @Test + void argumentsShouldBeAppliedToAllPartitions() { + int partitions = 3; + var argKey = "x-max-age"; + var argValue = 10_000; + + Map testArguments = Map.of(argKey, argValue); + SuperStream superStream = new SuperStream("stream", partitions, testArguments); + + List streams = superStream.getDeclarablesByType(Queue.class); + Assertions.assertEquals(partitions, streams.size()); + + streams.forEach( + it -> { + Object value = it.getArguments().get(argKey); + Assertions.assertNotNull(value, "Arg value should be present"); + Assertions.assertEquals(argValue, value, "Value should be the same"); + } + ); + } + + @Test + void testCustomPartitionsRoutingStrategy() { + var streamName = "test-super-stream-name"; + var partitions = 3; + var names = List.of("test.stream.1", "test.stream.2", "test.stream.3"); + + SuperStream superStream = SuperStreamBuilder.superStream(streamName, partitions) + .routingKeyStrategy((name, partition) -> names) + .build(); + + List bindings = superStream.getDeclarablesByType(Binding.class); + Set routingKeys = bindings.stream().map(Binding::getRoutingKey).collect(Collectors.toSet()); + Assertions.assertTrue(routingKeys.containsAll(names)); + } + + @Test + void builderMustSetupNameAndPartitionsNumber() { + var name = "test-super-stream-name"; + var partitions = 3; + SuperStream superStream = SuperStreamBuilder.superStream(name, partitions).build(); + List streams = superStream.getDeclarablesByType(Queue.class); + Assertions.assertEquals(partitions, streams.size()); + + streams.forEach(it -> Assertions.assertTrue(it.getName().startsWith(name))); + } + + @Test + void builderMustSetupArguments() { + var finalPartitionsNumber = 4; + var finalName = "test-name"; + var maxAge = "1D"; + var maxLength = 10_000_000L; + var maxSegmentsSize = 100_000L; + var initialClusterSize = 5; + + var testArgName = "test-key"; + var testArgValue = "test-value"; + + SuperStream superStream = SuperStreamBuilder.superStream("name", 3) + .partitions(finalPartitionsNumber) + .maxAge(maxAge) + .maxLength(maxLength) + .maxSegmentSize(maxSegmentsSize) + .initialClusterSize(initialClusterSize) + .name(finalName) + .withArgument(testArgName, testArgValue) + .build(); + + List streams = superStream.getDeclarablesByType(Queue.class); + + Assertions.assertEquals(finalPartitionsNumber, streams.size()); + streams.forEach( + it -> { + Assertions.assertTrue(it.getName().startsWith(finalName)); + Assertions.assertEquals(maxAge, it.getArguments().get("x-max-age")); + Assertions.assertEquals(maxLength, it.getArguments().get("max-length-bytes")); + Assertions.assertEquals(initialClusterSize, it.getArguments().get("x-initial-cluster-size")); + Assertions.assertEquals(maxSegmentsSize, it.getArguments().get("x-stream-max-segment-size-bytes")); + Assertions.assertEquals(testArgValue, it.getArguments().get(testArgName)); + } + ); + } + + @Test + void builderShouldForbidInternalArgumentsChanges() { + SuperStreamBuilder builder = SuperStreamBuilder.superStream("name", 3); + + Assertions.assertThrows(IllegalArgumentException.class, () -> builder.withArgument("x-queue-type", "quorum")); + } + + @Test + void nameCantBeEmpty() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("", 3).build() + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("testName", 3).name("").build() + ); + + Assertions.assertDoesNotThrow( + () -> SuperStreamBuilder.superStream("testName", 3).build() + ); + } + + @Test + void partitionsNumberShouldBeGreatThenZero() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("testName", 0).build() + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("testName", -1).build() + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("testName", 1).partitions(0).build() + ); + + Assertions.assertDoesNotThrow( + () -> SuperStreamBuilder.superStream("testName", 1).build() + ); + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java new file mode 100644 index 0000000000..d616d4a25e --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.config; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +@SpringJUnitConfig +@DirtiesContext +public class SuperStreamProvisioningTests extends AbstractTestContainerTests { + + @Test + void provision(@Autowired Declarables declarables, @Autowired CachingConnectionFactory cf, + @Autowired RabbitAdmin admin) { + + assertThat(declarables.getDeclarables()).hasSize(7); + cf.createConnection(); + List queues = declarables.getDeclarablesByType(Queue.class); + assertThat(queues).extracting(que -> que.getName()).contains("test-0", "test-1", "test-2"); + queues.forEach(que -> admin.deleteQueue(que.getName())); + declarables.getDeclarablesByType(DirectExchange.class).forEach(ex -> admin.deleteExchange(ex.getName())); + } + + @Configuration + public static class Config { + + @Bean + CachingConnectionFactory cf() { + return new CachingConnectionFactory("localhost", amqpPort()); + } + + @Bean + RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + @Bean + SuperStream superStream() { + return new SuperStream("test", 3); + } + + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index 5ce1602c3f..102855c959 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,81 +16,167 @@ package org.springframework.rabbit.stream.listener; -import static org.assertj.core.api.Assertions.assertThat; - +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.MessageHandler.Context; +import com.rabbitmq.stream.OffsetSpecification; +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.retry.StreamRetryOperationsInterceptorFactoryBean; +import org.springframework.rabbit.stream.support.StreamAdmin; +import org.springframework.rabbit.stream.support.StreamMessageProperties; +import org.springframework.retry.interceptor.RetryOperationsInterceptor; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriUtils; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; -import com.rabbitmq.stream.Address; -import com.rabbitmq.stream.Environment; -import com.rabbitmq.stream.Message; -import com.rabbitmq.stream.MessageHandler.Context; -import com.rabbitmq.stream.OffsetSpecification; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell + * @author Artem Bilan * @since 2.4 * */ @SpringJUnitConfig @DirtiesContext -public class RabbitListenerTests extends AbstractIntegrationTests { +public class RabbitListenerTests extends AbstractTestContainerTests { @Autowired Config config; @Test - void simple(@Autowired RabbitTemplate template) throws InterruptedException { - template.convertAndSend("test.stream.queue1", "foo"); + void simple(@Autowired RabbitStreamTemplate template, @Autowired MeterRegistry meterRegistry) throws Exception { + Future future = template.convertAndSend("foo"); + assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); + future = template.convertAndSend("bar", msg -> msg); + assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); + future = template.send(new org.springframework.amqp.core.Message("baz".getBytes(), + new StreamMessageProperties())); + assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); + future = template.send(template.messageBuilder().addData("qux".getBytes()).build()); + assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.config.latch1.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(this.config.received).isEqualTo("foo"); - assertThat(this.config.id).isEqualTo("test"); + assertThat(this.config.received).containsExactly("foo", "foo", "bar", "baz", "qux"); + assertThat(this.config.id).isEqualTo("testNative"); + MeterRegistryAssert.assertThat(meterRegistry) + .hasTimerWithNameAndTags("spring.rabbit.stream.template", + KeyValues.of("spring.rabbit.stream.template.name", "streamTemplate1")) + .hasTimerWithNameAndTags("spring.rabbit.stream.listener", + KeyValues.of("spring.rabbit.stream.listener.id", "obs")) + .hasTimerWithNameAndTags("spring.rabbitmq.listener", + KeyValues.of("listener.id", "notObs") + .and("queue", "test.stream.queue1")); } @Test - void nativeMsg(@Autowired RabbitTemplate template) throws InterruptedException { + void nativeMsg(@Autowired RabbitTemplate template, @Autowired MeterRegistry meterRegistry) + throws InterruptedException { + template.convertAndSend("test.stream.queue2", "foo"); + // Send a second to ensure the timer exists before the assertion + template.convertAndSend("test.stream.queue2", "bar"); assertThat(this.config.latch2.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.config.receivedNative).isNotNull(); assertThat(this.config.context).isNotNull(); + assertThat(this.config.latch3.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.config.latch4.await(10, TimeUnit.SECONDS)).isTrue(); + MeterRegistryAssert.assertThat(meterRegistry) + .hasTimerWithNameAndTags("spring.rabbit.stream.listener", + KeyValues.of("spring.rabbit.stream.listener.id", "testObsNative")) + .hasTimerWithNameAndTags("spring.rabbitmq.listener", + KeyValues.of("listener.id", "testNative")) + .hasTimerWithNameAndTags("spring.rabbitmq.listener", + KeyValues.of("listener.id", "testNativeFail")); } + @SuppressWarnings("unchecked") @Test void queueOverAmqp() throws Exception { - Client client = new Client("http://guest:guest@localhost:" + RABBITMQ.getMappedPort(15672) + "/api"); - QueueInfo queue = client.getQueue("/", "stream.created.over.amqp"); - assertThat(queue.getArguments().get("x-queue-type")).isEqualTo("stream"); + WebClient client = WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication("guest", "guest")) + .build(); + Map queue = queueInfo("stream.created.over.amqp"); + assertThat(((Map) queue.get("arguments")).get("x-queue-type")).isEqualTo("stream"); + } + + private Map queueInfo(String queueName) throws URISyntaxException { + WebClient client = createClient("guest", "guest"); + URI uri = queueUri(queueName); + return client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + } + + private URI queueUri(String queue) throws URISyntaxException { + return new URI("http://localhost:" + managementPort() + "/api") + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queue); + } + + private WebClient createClient(String adminUser, String adminPassword) { + return WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(adminUser, adminPassword)) + .build(); } @Configuration(proxyBeanMethods = false) @EnableRabbit public static class Config { - final CountDownLatch latch1 = new CountDownLatch(1); + final CountDownLatch latch1 = new CountDownLatch(9); + + final CountDownLatch latch2 = new CountDownLatch(4); + + final CountDownLatch latch3 = new CountDownLatch(6); - final CountDownLatch latch2 = new CountDownLatch(1); + final CountDownLatch latch4 = new CountDownLatch(1); - volatile String received; + final List received = new ArrayList<>(); + + final AtomicBoolean first = new AtomicBoolean(true); volatile Message receivedNative; @@ -99,58 +185,159 @@ public static class Config { volatile String id; @Bean - Environment environment() { + MeterRegistry meterReg() { + return new SimpleMeterRegistry(); + } + + @Bean + ObservationRegistry obsReg(MeterRegistry meterRegistry) { + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(new DefaultMeterObservationHandler(meterRegistry)); + return registry; + } + + @Bean + static Environment environment() { return Environment.builder() - .addressResolver(add -> new Address("localhost", RABBITMQ.getMappedPort(5552))) + .port(streamPort()) .build(); } @Bean - SmartLifecycle creator(Environment env) { + StreamAdmin streamAdmin(Environment env) { + StreamAdmin streamAdmin = new StreamAdmin(env, sc -> { + sc.stream("test.stream.queue1").create(); + sc.stream("test.stream.queue2").create(); + }); + streamAdmin.setAutoStartup(false); + return streamAdmin; + } + + @Bean + SmartLifecycle creator(Environment env, StreamAdmin admin) { return new SmartLifecycle() { + boolean running; + @Override public void stop() { + clean(env); + this.running = false; } @Override public void start() { - env.streamCreator().stream("test.stream.queue1").create(); - env.streamCreator().stream("test.stream.queue2").create(); + clean(env); + admin.start(); + this.running = true; + } + + private void clean(Environment env) { + try { + env.deleteStream("test.stream.queue1"); + } + catch (Exception e) { + } + try { + env.deleteStream("test.stream.queue2"); + } + catch (Exception e) { + } + try { + env.deleteStream("stream.created.over.amqp"); + } + catch (Exception e) { + } } @Override public boolean isRunning() { - return false; + return this.running; + } + + @Override + public int getPhase() { + return 0; } + }; } @Bean RabbitListenerContainerFactory rabbitListenerContainerFactory(Environment env) { - return new StreamRabbitListenerContainerFactory(env); + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setAdviceChain(RetryInterceptorBuilder.stateless().build()); + return factory; } - @RabbitListener(queues = "test.stream.queue1") + @Bean + RabbitListenerContainerFactory observableFactory(Environment env) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setObservationEnabled(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name(id) + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; + } + + @RabbitListener(id = "notObs", queues = "test.stream.queue1") void listen(String in) { - this.received = in; + this.received.add(in); + this.latch1.countDown(); + if (first.getAndSet(false)) { + throw new RuntimeException("fail first"); + } + } + + @RabbitListener(id = "obs", queues = "test.stream.queue1", containerFactory = "observableFactory") + void listenObs(String in) { this.latch1.countDown(); } @Bean - RabbitListenerContainerFactory nativeFactory(Environment env) { + public StreamRetryOperationsInterceptorFactoryBean sfb() { + StreamRetryOperationsInterceptorFactoryBean rfb = new StreamRetryOperationsInterceptorFactoryBean(); + rfb.setStreamMessageRecoverer((msg, context, throwable) -> this.latch4.countDown()); + return rfb; + } + + @Bean + @DependsOn("sfb") + RabbitListenerContainerFactory nativeFactory(Environment env, + RetryOperationsInterceptor retry) { + + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setNativeListener(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name(id) + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + if (id.equals("testNative")) { + this.id = id; + } + }); + factory.setAdviceChain(retry); + return factory; + } + + @Bean + RabbitListenerContainerFactory nativeObsFactory(Environment env, + RetryOperationsInterceptor retry) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); factory.setNativeListener(true); + factory.setObservationEnabled(true); factory.setConsumerCustomizer((id, builder) -> { - builder.name("myConsumer") + builder.name(id) .offset(OffsetSpecification.first()) .manualTrackingStrategy(); - this.id = id; }); return factory; } - @RabbitListener(id = "test", queues = "test.stream.queue2", containerFactory = "nativeFactory") + @RabbitListener(id = "testNative", queues = "test.stream.queue2", containerFactory = "nativeFactory") void nativeMsg(Message in, Context context) { this.receivedNative = in; this.context = context; @@ -158,9 +345,23 @@ void nativeMsg(Message in, Context context) { context.storeOffset(); } + @RabbitListener(id = "testNativeFail", queues = "test.stream.queue2", containerFactory = "nativeFactory") + void nativeMsgFail(Message in, Context context) { + this.latch3.countDown(); + throw new RuntimeException("fail all"); + } + + @RabbitListener(id = "testObsNative", queues = "test.stream.queue2", containerFactory = "nativeObsFactory") + void nativeObsMsg(Message in, Context context) { + this.receivedNative = in; + this.context = context; + this.latch2.countDown(); + context.storeOffset(); + } + @Bean CachingConnectionFactory cf() { - return new CachingConnectionFactory(RABBITMQ.getContainerIpAddress(), RABBITMQ.getFirstMappedPort()); + return new CachingConnectionFactory("localhost", amqpPort()); } @Bean @@ -168,6 +369,14 @@ RabbitTemplate template(CachingConnectionFactory cf) { return new RabbitTemplate(cf); } + @Bean + RabbitStreamTemplate streamTemplate1(Environment env) { + RabbitStreamTemplate template = new RabbitStreamTemplate(env, "test.stream.queue1"); + template.setProducerCustomizer((name, builder) -> builder.name("test")); + template.setObservationEnabled(true); + return template; + } + @Bean RabbitAdmin admin(CachingConnectionFactory cf) { return new RabbitAdmin(cf); @@ -180,6 +389,6 @@ Queue queue() { .build(); } - } + } } diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java new file mode 100644 index 0000000000..2b8cb7092b --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.listener; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import com.rabbitmq.stream.ConsumerBuilder; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.MessageHandler; +import com.rabbitmq.stream.MessageHandler.Context; +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; + +/** + * @author Gary Russell + * @since 2.4.5 + * + */ +public class StreamListenerContainerTests { + + @Test + void testAdviceChain() throws Exception { + Environment env = mock(Environment.class); + ConsumerBuilder builder = mock(ConsumerBuilder.class); + given(env.consumerBuilder()).willReturn(builder); + AtomicReference handler = new AtomicReference<>(); + willAnswer(inv -> { + handler.set(inv.getArgument(0)); + return null; + } + ).given(builder).messageHandler(any()); + AtomicBoolean advised = new AtomicBoolean(); + MethodInterceptor advice = (inv) -> { + advised.set(true); + return inv.proceed(); + }; + + StreamListenerContainer container = new StreamListenerContainer(env); + container.setAdviceChain(advice); + AtomicBoolean called = new AtomicBoolean(); + MessageListener ml = mock(MessageListener.class); + willAnswer(inv -> { + called.set(true); + return null; + }).given(ml).onMessage(any()); + container.setupMessageListener(ml); + Message message = mock(Message.class); + given(message.getBodyAsBinary()).willReturn("foo".getBytes()); + Context context = mock(Context.class); + handler.get().handle(context, message); + assertThat(advised.get()).isTrue(); + assertThat(called.get()).isTrue(); + + advised.set(false); + called.set(false); + ChannelAwareMessageListener cal = mock(ChannelAwareMessageListener.class); + willAnswer(inv -> { + called.set(true); + return null; + }).given(cal).onMessage(any(), isNull()); + container.setupMessageListener(cal); + handler.get().handle(context, message); + assertThat(advised.get()).isTrue(); + assertThat(called.get()).isTrue(); + + called.set(false); + StreamMessageListener sml = mock(StreamMessageListener.class); + willAnswer(inv -> { + called.set(true); + return null; + }).given(sml).onStreamMessage(message, context); + container.setupMessageListener(sml); + handler.get().handle(context, message); + assertThat(advised.get()).isTrue(); + assertThat(called.get()).isTrue(); + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java new file mode 100644 index 0000000000..0ca49fb872 --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.listener; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.OffsetSpecification; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.rabbit.stream.config.SuperStream; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @author Artem Bilan + * @since 3.0 + * + */ +@SpringJUnitConfig +@DirtiesContext +public class SuperStreamConcurrentSACTests extends AbstractTestContainerTests { + + @Test + void concurrent(@Autowired StreamListenerContainer container, @Autowired RabbitTemplate template, + @Autowired Config config, @Autowired RabbitAdmin admin, + @Autowired Declarables superStream) throws InterruptedException { + + template.getConnectionFactory().createConnection(); + container.start(); + assertThat(config.consumerLatch.await(10, TimeUnit.SECONDS)).isTrue(); + template.convertAndSend("ss.sac.concurrency.test", "0", "foo"); + template.convertAndSend("ss.sac.concurrency.test", "1", "bar"); + template.convertAndSend("ss.sac.concurrency.test", "2", "baz"); + assertThat(config.messageLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(config.threads).hasSize(3); + container.stop(); + clean(admin, superStream); + } + + private void clean(RabbitAdmin admin, Declarables declarables) { + declarables.getDeclarablesByType(Queue.class).forEach(queue -> admin.deleteQueue(queue.getName())); + declarables.getDeclarablesByType(DirectExchange.class).forEach(ex -> admin.deleteExchange(ex.getName())); + } + + @Configuration + public static class Config { + + final Set threads = new HashSet<>(); + + final CountDownLatch consumerLatch = new CountDownLatch(3); + + final CountDownLatch messageLatch = new CountDownLatch(3); + + @Bean + CachingConnectionFactory cf() { + return new CachingConnectionFactory("localhost", amqpPort()); + } + + @Bean + RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + @Bean + RabbitTemplate template(ConnectionFactory cf) { + return new RabbitTemplate(cf); + } + + @Bean + SuperStream superStream() { + return new SuperStream("ss.sac.concurrency.test", 3); + } + + @Bean + static Environment environment() { + return Environment.builder() + .port(streamPort()) + .maxConsumersByConnection(1) + .build(); + } + + @Bean + StreamListenerContainer concurrentContainer(Environment env) { + StreamListenerContainer container = new StreamListenerContainer(env); + container.superStream("ss.sac.concurrency.test", "concurrent", 3); + container.setupMessageListener(msg -> { + this.threads.add(Thread.currentThread().getName()); + this.messageLatch.countDown(); + }); + container.setConsumerCustomizer((id, builder) -> { + builder.consumerUpdateListener(context -> { + this.consumerLatch.countDown(); + return OffsetSpecification.last(); + }); + }); + container.setAutoStartup(false); + return container; + } + + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java new file mode 100644 index 0000000000..f14a3947dd --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.listener; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.OffsetSpecification; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.rabbit.stream.config.SuperStream; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @author Artem Bilan + * @since 3.0 + * + */ +@SpringJUnitConfig +@DirtiesContext +public class SuperStreamSACTests extends AbstractTestContainerTests { + + @Test + void superStream(@Autowired ApplicationContext context, @Autowired RabbitTemplate template, + @Autowired Environment env, @Autowired Config config, @Autowired RabbitAdmin admin, + @Autowired Declarables declarables) throws InterruptedException { + + template.getConnectionFactory().createConnection(); + StreamListenerContainer container1 = context.getBean(StreamListenerContainer.class, env, "one"); + container1.start(); + StreamListenerContainer container2 = context.getBean(StreamListenerContainer.class, env, "two"); + container2.start(); + StreamListenerContainer container3 = context.getBean(StreamListenerContainer.class, env, "three"); + container3.start(); + template.convertAndSend("ss.sac.test", "rk-0", "foo"); + template.convertAndSend("ss.sac.test", "rk-1", "bar"); + template.convertAndSend("ss.sac.test", "rk-2", "baz"); + assertThat(config.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(config.messages.keySet()).contains("one", "two", "three"); + assertThat(config.info).contains("one:foo", "two:bar", "three:baz"); + container1.stop(); + container2.stop(); + container3.stop(); + clean(admin, declarables); + } + + private void clean(RabbitAdmin admin, Declarables declarables) { + declarables.getDeclarablesByType(Queue.class).forEach(queue -> admin.deleteQueue(queue.getName())); + declarables.getDeclarablesByType(DirectExchange.class).forEach(ex -> admin.deleteExchange(ex.getName())); + } + + @Configuration + public static class Config { + + final List info = new ArrayList<>(); + + final Map messages = new ConcurrentHashMap<>(); + + final CountDownLatch latch = new CountDownLatch(3); + + @Bean + CachingConnectionFactory cf() { + return new CachingConnectionFactory("localhost", amqpPort()); + } + + @Bean + RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + @Bean + RabbitTemplate template(ConnectionFactory cf) { + return new RabbitTemplate(cf); + } + + @Bean + SuperStream superStream() { + return new SuperStream("ss.sac.test", 3, (q, i) -> IntStream.range(0, i) + .mapToObj(j -> "rk-" + j) + .collect(Collectors.toList())); + } + + @Bean + static Environment environment() { + return Environment.builder() + .port(streamPort()) + .build(); + } + + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + StreamListenerContainer container(Environment env, String name) { + StreamListenerContainer container = new StreamListenerContainer(env); + container.superStream("ss.sac.test", "test"); + container.setupMessageListener(msg -> { + this.messages.put(name, msg); + this.info.add(name + ":" + new String(msg.getBody())); + this.latch.countDown(); + }); + container.setConsumerCustomizer((id, builder) -> builder.offset(OffsetSpecification.last())); + container.setAutoStartup(false); + return container; + } + + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java new file mode 100644 index 0000000000..14654159a5 --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java @@ -0,0 +1,217 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.micrometer; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.OffsetSpecification; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Span.Kind; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.simple.SpanAssert; +import io.micrometer.tracing.test.simple.SpansAssert; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; +import org.springframework.rabbit.stream.listener.StreamListenerContainer; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.support.StreamAdmin; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @author Artem Bilan + * @since 3.0.5 + * + */ +@Testcontainers(disabledWithoutDocker = true) +public class TracingTests extends SampleTestRunner { + + private static final AbstractTestContainerTests atct = new AbstractTestContainerTests() { + }; + + @Override + public SampleTestRunnerConsumer yourCode() throws Exception { + return (bb, meterRegistry) -> { + ObservationRegistry observationRegistry = getObservationRegistry(); + try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) { + applicationContext.getBeanFactory().registerSingleton("obsReg", observationRegistry); + applicationContext.register(Config.class); + applicationContext.refresh(); + applicationContext.getBean(RabbitStreamTemplate.class).convertAndSend("test").get(10, TimeUnit.SECONDS); + assertThat(applicationContext.getBean(Listener.class).latch1.await(10, TimeUnit.SECONDS)).isTrue(); + } + + List finishedSpans = bb.getFinishedSpans(); + SpansAssert.assertThat(finishedSpans) + .haveSameTraceId() + .hasSize(3); + List producerSpans = finishedSpans.stream() + .filter(span -> span.getKind().equals(Kind.PRODUCER)) + .collect(Collectors.toList()); + List consumerSpans = finishedSpans.stream() + .filter(span -> span.getKind().equals(Kind.CONSUMER)) + .collect(Collectors.toList()); + SpanAssert.assertThat(producerSpans.get(0)) + .hasTag("spring.rabbit.stream.template.name", "streamTemplate1"); + SpanAssert.assertThat(producerSpans.get(0)) + .hasRemoteServiceNameEqualTo("RabbitMQ Stream"); + SpanAssert.assertThat(consumerSpans.get(0)) + .hasTagWithKey("spring.rabbit.stream.listener.id"); + SpanAssert.assertThat(consumerSpans.get(0)) + .hasRemoteServiceNameEqualTo("RabbitMQ Stream"); + assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.stream.listener.id")).isIn("one", "two"); + SpanAssert.assertThat(consumerSpans.get(1)) + .hasTagWithKey("spring.rabbit.stream.listener.id"); + assertThat(consumerSpans.get(1).getTags().get("spring.rabbit.stream.listener.id")).isIn("one", "two"); + assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.stream.listener.id")) + .isNotEqualTo(consumerSpans.get(1).getTags().get("spring.rabbit.stream.listener.id")); + }; + } + + @EnableRabbit + @Configuration(proxyBeanMethods = false) + public static class Config { + + @Bean + static Environment environment() { + return Environment.builder() + .port(AbstractTestContainerTests.streamPort()) + .build(); + } + + @Bean + StreamAdmin streamAdmin(Environment env) { + StreamAdmin streamAdmin = new StreamAdmin(env, sc -> { + sc.stream("trace.stream.queue1").create(); + }); + streamAdmin.setAutoStartup(false); + return streamAdmin; + } + + @Bean + SmartLifecycle creator(Environment env, StreamAdmin admin) { + return new SmartLifecycle() { + + boolean running; + + @Override + public void stop() { + clean(env); + this.running = false; + } + + @Override + public void start() { + clean(env); + admin.start(); + this.running = true; + } + + private void clean(Environment env) { + try { + env.deleteStream("trace.stream.queue1"); + } + catch (Exception e) { + } + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public int getPhase() { + return 0; + } + + }; + } + + @Bean + RabbitListenerContainerFactory rabbitListenerContainerFactory(Environment env) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setObservationEnabled(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name(id) + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; + } + + @Bean + RabbitListenerContainerFactory nativeFactory(Environment env) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setNativeListener(true); + factory.setObservationEnabled(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name(id) + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; + } + + @Bean + RabbitStreamTemplate streamTemplate1(Environment env) { + RabbitStreamTemplate template = new RabbitStreamTemplate(env, "trace.stream.queue1"); + template.setProducerCustomizer((name, builder) -> builder.name("test")); + template.setObservationEnabled(true); + return template; + } + + @Bean + Listener listener() { + return new Listener(); + } + + } + + public static class Listener { + + CountDownLatch latch1 = new CountDownLatch(2); + + @RabbitListener(id = "one", queues = "trace.stream.queue1") + void listen(String in) { + latch1.countDown(); + } + + @RabbitListener(id = "two", queues = "trace.stream.queue1", containerFactory = "nativeFactory") + public void listen(Message in) { + latch1.countDown(); + } + + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java new file mode 100644 index 0000000000..f1dd52d432 --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.rabbit.stream.producer; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import com.rabbitmq.stream.ConfirmationHandler; +import com.rabbitmq.stream.ConfirmationStatus; +import com.rabbitmq.stream.Constants; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.Producer; +import com.rabbitmq.stream.ProducerBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * @author Gary Russell + * @since 2.4.7 + * + */ +public class RabbitStreamTemplateTests { + + @Test + void handleConfirm() throws InterruptedException, ExecutionException { + Environment env = mock(Environment.class); + ProducerBuilder pb = mock(ProducerBuilder.class); + given(env.producerBuilder()).willReturn(pb); + Producer producer = mock(Producer.class); + given(pb.build()).willReturn(producer); + AtomicInteger which = new AtomicInteger(); + willAnswer(inv -> { + ConfirmationHandler handler = inv.getArgument(1); + ConfirmationStatus status = null; + switch (which.getAndIncrement()) { + case 0: + status = new ConfirmationStatus(inv.getArgument(0), true, (short) 0); + break; + case 1: + status = new ConfirmationStatus(inv.getArgument(0), false, Constants.CODE_MESSAGE_ENQUEUEING_FAILED); + break; + case 2: + status = new ConfirmationStatus(inv.getArgument(0), false, Constants.CODE_PRODUCER_CLOSED); + break; + case 3: + status = new ConfirmationStatus(inv.getArgument(0), false, Constants.CODE_PRODUCER_NOT_AVAILABLE); + break; + case 4: + status = new ConfirmationStatus(inv.getArgument(0), false, Constants.CODE_PUBLISH_CONFIRM_TIMEOUT); + break; + case 5: + status = new ConfirmationStatus(inv.getArgument(0), false, (short) -1); + break; + } + handler.handle(status); + return null; + }).given(producer).send(any(), any()); + try (RabbitStreamTemplate template = new RabbitStreamTemplate(env, "foo")) { + SimpleMessageConverter messageConverter = new SimpleMessageConverter(); + template.setMessageConverter(messageConverter); + assertThat(template.messageConverter()).isSameAs(messageConverter); + StreamMessageConverter converter = mock(StreamMessageConverter.class); + given(converter.fromMessage(any())).willReturn(mock(Message.class)); + template.setStreamConverter(converter); + assertThat(template.streamMessageConverter()).isSameAs(converter); + CompletableFuture future = template.convertAndSend("foo"); + assertThat(future.get()).isTrue(); + CompletableFuture future1 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future1.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Message Enqueueing Failed"); + CompletableFuture future2 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future2.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Producer Closed"); + CompletableFuture future3 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future3.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Producer Not Available"); + CompletableFuture future4 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future4.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Publish Confirm Timeout"); + CompletableFuture future5 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future5.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Unknown code: " + -1); + } + } + + @Test + void superStream() { + Environment env = mock(Environment.class); + ProducerBuilder pb = mock(ProducerBuilder.class); + given(pb.superStream(any())).willReturn(pb); + given(env.producerBuilder()).willReturn(pb); + Producer producer = mock(Producer.class); + given(pb.build()).willReturn(producer); + try (RabbitStreamTemplate template = new RabbitStreamTemplate(env, "foo")) { + SimpleMessageConverter messageConverter = new SimpleMessageConverter(); + template.setMessageConverter(messageConverter); + assertThat(template.messageConverter()).isSameAs(messageConverter); + StreamMessageConverter converter = mock(StreamMessageConverter.class); + given(converter.fromMessage(any())).willReturn(mock(Message.class)); + template.setStreamConverter(converter); + template.setSuperStreamRouting(msg -> "bar"); + template.convertAndSend("x"); + verify(pb).superStream("foo"); + verify(pb).routing(any()); + verify(pb, never()).stream("foo"); + } + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverterTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverterTests.java index 342e138302..c63d682f4e 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverterTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,15 @@ package org.springframework.rabbit.stream.support.converter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - +import com.rabbitmq.stream.MessageHandler.Context; +import com.rabbitmq.stream.Properties; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; import org.springframework.rabbit.stream.support.StreamMessageProperties; -import com.rabbitmq.stream.MessageHandler.Context; -import com.rabbitmq.stream.Properties; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java index 2c8c0e2147..beb74a6088 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.mockito.AdditionalAnswers; import org.mockito.Mockito; @@ -41,7 +42,7 @@ import org.springframework.amqp.rabbit.test.mockito.LatchCountDownAndCallRealMethodAnswer; import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.AnnotationMetadata; import org.springframework.test.util.AopTestUtils; import org.springframework.util.Assert; @@ -73,13 +74,15 @@ public class RabbitListenerTestHarness extends RabbitListenerAnnotationBeanPostP private final AnnotationAttributes attributes; public RabbitListenerTestHarness(AnnotationMetadata importMetadata) { - Map map = importMetadata.getAnnotationAttributes(RabbitListenerTest.class.getName()); - this.attributes = AnnotationAttributes.fromMap(map); - Assert.notNull(this.attributes, + Map map = importMetadata.getAnnotationAttributes(RabbitListenerTest.class.getName()); + AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(map); + Assert.notNull(annotationAttributes, () -> "@RabbitListenerTest is not present on importing class " + importMetadata.getClassName()); + this.attributes = annotationAttributes; } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected Collection processListener(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener, Object bean, Object target, String beanName) { @@ -110,7 +113,7 @@ protected Collection processListener(MethodRabbitListenerEndpoint en else { logger.info("The test harness can only proxy @RabbitListeners with an 'id' attribute"); } - return super.processListener(endpoint, rabbitListener, proxy, target, beanName); // NOSONAR proxy is not null + return super.processListener(endpoint, rabbitListener, proxy, target, beanName); } /** @@ -138,7 +141,9 @@ public LambdaAnswer getLambdaAnswerFor(String id, boolean callRealMethod, return new LambdaAnswer<>(callRealMethod, callback, this.delegates.get(id)); } - public InvocationData getNextInvocationDataFor(String id, long wait, TimeUnit unit) throws InterruptedException { + public @Nullable InvocationData getNextInvocationDataFor(String id, long wait, TimeUnit unit) + throws InterruptedException { + CaptureAdvice advice = this.listenerCapture.get(id); if (advice != null) { return advice.invocationData.poll(wait, unit); @@ -147,7 +152,7 @@ public InvocationData getNextInvocationDataFor(String id, long wait, TimeUnit un } @SuppressWarnings("unchecked") - public T getSpy(String id) { + public @Nullable T getSpy(String id) { return (T) this.listeners.get(id); } @@ -159,7 +164,7 @@ public T getSpy(String id) { * @since 2.1.16 */ @SuppressWarnings("unchecked") - public T getDelegate(String id) { + public @Nullable T getDelegate(String id) { return (T) this.delegates.get(id); } @@ -171,10 +176,10 @@ private static final class CaptureAdvice implements MethodInterceptor { } @Override - public Object invoke(MethodInvocation invocation) throws Throwable { - boolean isListenerMethod = - AnnotationUtils.findAnnotation(invocation.getMethod(), RabbitListener.class) != null - || AnnotationUtils.findAnnotation(invocation.getMethod(), RabbitHandler.class) != null; + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { + MergedAnnotations annotations = MergedAnnotations.from(invocation.getMethod()); + boolean isListenerMethod = annotations.isPresent(RabbitListener.class) + || annotations.isPresent(RabbitHandler.class); try { Object result = invocation.proceed(); if (isListenerMethod) { @@ -196,11 +201,11 @@ public static class InvocationData { private final MethodInvocation invocation; - private final Object result; + private final @Nullable Object result; - private final Throwable throwable; + private final @Nullable Throwable throwable; - public InvocationData(MethodInvocation invocation, Object result) { + public InvocationData(MethodInvocation invocation, @Nullable Object result) { this.invocation = invocation; this.result = result; this.throwable = null; @@ -212,15 +217,15 @@ public InvocationData(MethodInvocation invocation, Throwable throwable) { this.throwable = throwable; } - public Object[] getArguments() { + public @Nullable Object[] getArguments() { return this.invocation.getArguments(); } - public Object getResult() { + public @Nullable Object getResult() { return this.result; } - public Throwable getThrowable() { + public @Nullable Throwable getThrowable() { return this.throwable; } diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java index 246cf0dbd3..f3e2af5891 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.test; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; - import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -29,8 +23,15 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Envelope; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageBuilder; import org.springframework.amqp.core.MessageListener; @@ -48,10 +49,13 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.util.Assert; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Envelope; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; /** * A {@link RabbitTemplate} that invokes {@code @RabbitListener} s directly. @@ -60,6 +64,7 @@ * * @author Gary Russell * @author Artem Bilan + * @author Christian Tzolov * * @since 2.0 * @@ -71,11 +76,13 @@ public class TestRabbitTemplate extends RabbitTemplate private final Map listeners = new HashMap<>(); + @SuppressWarnings("NullAway.Init") private ApplicationContext applicationContext; @Autowired private RabbitListenerEndpointRegistry registry; + @SuppressWarnings("this-escape") public TestRabbitTemplate(ConnectionFactory connectionFactory) { super(connectionFactory); setReplyAddress(REPLY_QUEUE); @@ -105,7 +112,9 @@ public void onApplicationEvent(ContextRefreshedEvent event) { } private void setupListener(AbstractMessageListenerContainer container, String queue) { - this.listeners.computeIfAbsent(queue, v -> new Listeners()).listeners.add(container.getMessageListener()); + MessageListener messageListener = container.getMessageListener(); + Assert.notNull(messageListener, "'container.getMessageListener()' must not be null"); + this.listeners.computeIfAbsent(queue, v -> new Listeners()).listeners.add(messageListener); } @Override @@ -130,8 +139,8 @@ protected void sendToRabbit(Channel channel, String exchange, String routingKey, } @Override - protected Message doSendAndReceiveWithFixed(String exchange, String routingKey, Message message, - CorrelationData correlationData) { + protected @Nullable Message doSendAndReceiveWithFixed(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData) { Listeners listenersForRoute = this.listeners.get(routingKey); if (listenersForRoute == null) { @@ -140,9 +149,8 @@ protected Message doSendAndReceiveWithFixed(String exchange, String routingKey, Channel channel = mock(Channel.class); final AtomicReference reply = new AtomicReference<>(); Object listener = listenersForRoute.next(); - if (listener instanceof AbstractAdaptableMessageListener) { + if (listener instanceof AbstractAdaptableMessageListener adapter) { try { - AbstractAdaptableMessageListener adapter = (AbstractAdaptableMessageListener) listener; willAnswer(i -> { Envelope envelope = new Envelope(1, false, "", REPLY_QUEUE); reply.set(MessageBuilder.withBody(i.getArgument(4)) // NOSONAR magic # @@ -161,22 +169,22 @@ protected Message doSendAndReceiveWithFixed(String exchange, String routingKey, } } else { - throw new IllegalStateException("sendAndReceive not supported for " + listener.getClass().getName()); + throw new IllegalStateException("sendAndReceive not supported for " + listener); } return reply.get(); } private void invoke(Object listener, Message message, Channel channel) { - if (listener instanceof ChannelAwareMessageListener) { + if (listener instanceof ChannelAwareMessageListener channelAwareMessageListener) { try { - ((ChannelAwareMessageListener) listener).onMessage(message, channel); + channelAwareMessageListener.onMessage(message, channel); } catch (Exception e) { throw RabbitExceptionTranslator.convertRabbitAccessException(e); } } - else if (listener instanceof MessageListener) { - ((MessageListener) listener).onMessage(message); + else if (listener instanceof MessageListener messageListener) { + messageListener.onMessage(message); } else { // Not really necessary since the container doesn't allow it, but no hurt @@ -186,18 +194,28 @@ else if (listener instanceof MessageListener) { private static class Listeners { + private final Lock lock = new ReentrantLock(); + private final List listeners = new ArrayList<>(); - private volatile Iterator iterator; + private volatile @Nullable Iterator iterator; Listeners() { } - private synchronized Object next() { - if (this.iterator == null || !this.iterator.hasNext()) { - this.iterator = this.listeners.iterator(); + private Object next() { + this.lock.lock(); + try { + Iterator iteratorToUse = this.iterator; + if (iteratorToUse == null || !iteratorToUse.hasNext()) { + iteratorToUse = this.listeners.iterator(); + } + this.iterator = iteratorToUse; + return iteratorToUse.next(); + } + finally { + this.lock.unlock(); } - return this.iterator.next(); } } diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/SpringRabbitContextCustomizerFactory.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/SpringRabbitContextCustomizerFactory.java index 8d7cc16d72..1367702c32 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/SpringRabbitContextCustomizerFactory.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/SpringRabbitContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; @@ -36,7 +38,7 @@ class SpringRabbitContextCustomizerFactory implements ContextCustomizerFactory { @Override - public ContextCustomizer createContextCustomizer(Class testClass, + public @Nullable ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { SpringRabbitTest test = AnnotatedElementUtils.findMergedAnnotation(testClass, SpringRabbitTest.class); diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/package-info.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/package-info.java index 3f1e810f32..47f51eeb29 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/package-info.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes relating to the test application context. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.test.context; diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java index 191fc447f1..82d16d499b 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author 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,15 +17,14 @@ package org.springframework.amqp.rabbit.test.mockito; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations; import org.mockito.invocation.InvocationOnMock; -import org.springframework.lang.Nullable; - /** * An {@link org.mockito.stubbing.Answer} to optionally call the real method and allow * returning a custom result. Captures any exceptions thrown. @@ -33,6 +32,8 @@ * @param the return type. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6 * */ @@ -43,21 +44,10 @@ public class LambdaAnswer extends ForwardsInvocations { private final ValueToReturn callback; - private final Set exceptions = Collections.synchronizedSet(new LinkedHashSet<>()); + private final Set exceptions = ConcurrentHashMap.newKeySet(); private final boolean hasDelegate; - /** - * Deprecated. - * @param callRealMethod true to call the real method. - * @param callback the callback. - * @deprecated in favor of {@link #LambdaAnswer(boolean, ValueToReturn, Object)}. - */ - @Deprecated - public LambdaAnswer(boolean callRealMethod, ValueToReturn callback) { - this(callRealMethod, callback, null); - } - /** * Construct an instance with the provided properties. Use the test harness to get an * instance with the proper delegate. @@ -99,15 +89,13 @@ public T answer(InvocationOnMock invocation) throws Throwable { * @since 2.2.3 */ public Collection getExceptions() { - synchronized (this.exceptions) { - return new LinkedHashSet<>(this.exceptions); - } + return new LinkedHashSet<>(this.exceptions); } @FunctionalInterface public interface ValueToReturn { - T apply(InvocationOnMock invocation, T result); + T apply(InvocationOnMock invocation, @Nullable T result); } diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java index fac417b2a9..bd3dd8510b 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,47 +16,39 @@ package org.springframework.amqp.rabbit.test.mockito; +import java.io.Serial; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations; import org.mockito.invocation.InvocationOnMock; -import org.springframework.lang.Nullable; - /** * An {@link org.mockito.stubbing.Answer} for void returning methods that calls the real * method and counts down a latch. Captures any exceptions thrown. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6 * */ public class LatchCountDownAndCallRealMethodAnswer extends ForwardsInvocations { + @Serial private static final long serialVersionUID = 1L; private final transient CountDownLatch latch; - private final Set exceptions = Collections.synchronizedSet(new LinkedHashSet<>()); + private final transient Set exceptions = ConcurrentHashMap.newKeySet(); private final boolean hasDelegate; - /** - * Get an instance with no delegate. - * @deprecated in favor of - * {@link #LatchCountDownAndCallRealMethodAnswer(int, Object)}. - * @param count to set in a {@link CountDownLatch}. - */ - @Deprecated - public LatchCountDownAndCallRealMethodAnswer(int count) { - this(count, null); - } - /** * Get an instance with the provided properties. Use the test harness to get an * instance with the proper delegate. @@ -71,7 +63,7 @@ public LatchCountDownAndCallRealMethodAnswer(int count, @Nullable Object delegat } @Override - public Object answer(InvocationOnMock invocation) throws Throwable { + public @Nullable Object answer(InvocationOnMock invocation) throws Throwable { try { if (this.hasDelegate) { return super.answer(invocation); @@ -112,9 +104,7 @@ public CountDownLatch getLatch() { */ @Nullable public Collection getExceptions() { - synchronized (this.exceptions) { - return new LinkedHashSet<>(this.exceptions); - } + return new LinkedHashSet<>(this.exceptions); } } diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/package-info.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/package-info.java index 9ac27fe258..f4cf10b7a1 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/package-info.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/package-info.java @@ -1,4 +1,5 @@ /** * Mockito extensions for testing Spring AMQP applications. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.test.mockito; diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/package-info.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/package-info.java index d592c60ae6..f1b17543c1 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/package-info.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/package-info.java @@ -1,4 +1,5 @@ /** * Classes for testing Spring AMQP applications. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.test; diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/repeatable/AbstractRabbitAnnotationDrivenTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/repeatable/AbstractRabbitAnnotationDrivenTests.java index 289d230bce..9b826d6492 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/repeatable/AbstractRabbitAnnotationDrivenTests.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/repeatable/AbstractRabbitAnnotationDrivenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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.amqp.rabbit.repeatable; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.annotation.RabbitHandler; @@ -27,6 +25,8 @@ import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; +import static org.assertj.core.api.Assertions.assertThat; + /** * * @author Stephane Nicoll diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/RabbitListenerProxyTest.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/RabbitListenerProxyTest.java index 20395cedb7..cc89938ef7 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/RabbitListenerProxyTest.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/RabbitListenerProxyTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.verify; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.AnonymousQueue; @@ -41,6 +36,11 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.verify; + /** * @author Miguel Gross Valle * diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java index b4c2fc7e01..8a2ebf65a7 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 the original author or 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.amqp.rabbit.test.context; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.config.AbstractRabbitListenerContainerFactory; @@ -29,8 +27,11 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.3 @@ -39,6 +40,7 @@ @RabbitAvailable @SpringJUnitConfig @SpringRabbitTest +@DirtiesContext public class SpringRabbitTestTests { @Autowired diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java index a70becefdd..e01b6a0db2 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or 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.amqp.rabbit.test.examples; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -46,6 +44,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.support.AnnotationConfigContextLoader; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @author Artem Bilan @@ -139,6 +139,11 @@ public Queue queue2() { return new AnonymousQueue(); } + @Bean + public Queue queue3() { + return new AnonymousQueue(); + } + @Bean public RabbitAdmin admin(ConnectionFactory cf) { return new RabbitAdmin(cf); @@ -168,6 +173,7 @@ public String foo(String foo) { } @RabbitListener(id = "bar", queues = "#{queue2.name}") + @RabbitListener(id = "bar2", queues = "#{queue3.name}") public void foo(@Payload String foo, @Header("amqp_receivedRoutingKey") String rk) { if (!failed && foo.equals("ex")) { failed = true; diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyAndCaptureTest.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyAndCaptureTest.java index 03349f9e23..0cc500f3a2 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyAndCaptureTest.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyAndCaptureTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.test.examples; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.verify; - import java.util.Collection; import java.util.concurrent.TimeUnit; @@ -47,6 +42,11 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @since 1.6 diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyTest.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyTest.java index 45cddf63b5..6c35eda971 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyTest.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.test.examples; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.verify; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.AnonymousQueue; @@ -44,6 +39,11 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @author Artem Bilan diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java index 8063132372..39b6a48f4b 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,10 @@ package org.springframework.amqp.rabbit.test.examples; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; - import java.io.IOException; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.annotation.EnableRabbit; @@ -39,10 +33,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; /** @@ -53,6 +53,7 @@ * */ @SpringJUnitConfig +@DirtiesContext public class TestRabbitTemplateTests { @Autowired @@ -135,6 +136,7 @@ public String baz(String in) { public SimpleMessageListenerContainer smlc1() throws IOException { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory()); container.setQueueNames("foo", "bar"); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new Object() { @SuppressWarnings("unused") diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java index f3e760be66..380b99c09d 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,16 @@ package org.springframework.amqp.rabbit.test.mockito; +import java.util.Collection; + +import org.junit.jupiter.api.Test; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.spy; -import java.util.Collection; - -import org.junit.jupiter.api.Test; - /** * @author Gary Russell * @since 1.6 diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java index 21b08d00ec..cb3e8a4ceb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,24 @@ package org.springframework.amqp.rabbit; -import java.util.Date; +import java.time.Instant; +import java.util.Objects; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; -import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.AmqpMessageReturnedException; -import org.springframework.amqp.core.AmqpReplyTimeoutException; import org.springframework.amqp.core.AsyncAmqpTemplate; import org.springframework.amqp.core.Correlation; import org.springframework.amqp.core.Message; @@ -47,6 +51,7 @@ import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; +import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SmartMessageConverter; import org.springframework.amqp.utils.JavaUtils; @@ -54,23 +59,17 @@ import org.springframework.context.SmartLifecycle; import org.springframework.core.ParameterizedTypeReference; import org.springframework.expression.Expression; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; - -import com.rabbitmq.client.Channel; /** - * Provides asynchronous send and receive operations returning a {@link ListenableFuture} + * Provides asynchronous send and receive operations returning a {@link CompletableFuture} * allowing the caller to obtain the reply later, using {@code get()} or a callback. *

* When confirms are enabled, the future has a confirm property which is itself a - * {@link ListenableFuture}. If the reply is received before the publisher confirm, + * {@link CompletableFuture}. If the reply is received before the publisher confirm, * the confirm is discarded since the reply implicitly indicates the message was * published. *

@@ -87,6 +86,9 @@ * * @author Gary Russell * @author Artem Bilan + * @author FengYang Su + * @author Ngoc Nhan + * @author Ben Efrati * * @since 1.6 */ @@ -97,13 +99,15 @@ public class AsyncRabbitTemplate implements AsyncAmqpTemplate, ChannelAwareMessa private final Log logger = LogFactory.getLog(this.getClass()); + private final Lock lock = new ReentrantLock(); + private final RabbitTemplate template; - private final AbstractMessageListenerContainer container; + private final @Nullable AbstractMessageListenerContainer container; - private final DirectReplyToMessageListenerContainer directReplyToContainer; + private final @Nullable DirectReplyToMessageListenerContainer directReplyToContainer; - private final String replyAddress; + private final @Nullable String replyAddress; private final ConcurrentMap> pending = new ConcurrentHashMap<>(); @@ -119,8 +123,10 @@ public class AsyncRabbitTemplate implements AsyncAmqpTemplate, ChannelAwareMessa private boolean autoStartup = true; + @SuppressWarnings("NullAway.Init") private String beanName; + @SuppressWarnings("NullAway.Init") private TaskScheduler taskScheduler; private boolean internalTaskScheduler = true; @@ -136,13 +142,14 @@ public class AsyncRabbitTemplate implements AsyncAmqpTemplate, ChannelAwareMessa */ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey, String replyQueue) { + this(connectionFactory, exchange, routingKey, replyQueue, null); } /** * Construct an instance using the provided arguments. If 'replyAddress' is null, * replies will be routed to the default exchange using the reply queue name as the - * routing key. Otherwise it should have the form exchange/routingKey and must + * routing key. Otherwise, it should have the form exchange/routingKey and must * cause messages to be routed to the reply queue. * @param connectionFactory the connection factory. * @param exchange the default exchange to which requests will be sent. @@ -150,8 +157,10 @@ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, * @param replyQueue the name of the reply queue to listen for replies. * @param replyAddress the reply address (exchange/routingKey). */ - public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey, - String replyQueue, String replyAddress) { + @SuppressWarnings("this-escape") + public AsyncRabbitTemplate(ConnectionFactory connectionFactory, @Nullable String exchange, String routingKey, + String replyQueue, @Nullable String replyAddress) { + Assert.notNull(connectionFactory, "'connectionFactory' cannot be null"); Assert.notNull(routingKey, "'routingKey' cannot be null"); Assert.notNull(replyQueue, "'replyQueue' cannot be null"); @@ -167,12 +176,7 @@ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, this.container.setMessageListener(this); this.container.afterPropertiesSet(); this.directReplyToContainer = null; - if (replyAddress == null) { - this.replyAddress = replyQueue; - } - else { - this.replyAddress = replyAddress; - } + this.replyAddress = Objects.requireNonNullElse(replyAddress, replyQueue); } @@ -191,26 +195,23 @@ public AsyncRabbitTemplate(RabbitTemplate template, AbstractMessageListenerConta * Construct an instance using the provided arguments. The first queue the container * is configured to listen to will be used as the reply queue. If 'replyAddress' is * null, replies will be routed using the default exchange with that queue name as the - * routing key. Otherwise it should have the form exchange/routingKey and must + * routing key. Otherwise, it should have the form exchange/routingKey and must * cause messages to be routed to the reply queue. * @param template a {@link RabbitTemplate}. * @param container a {@link AbstractMessageListenerContainer}. * @param replyAddress the reply address. */ + @SuppressWarnings("this-escape") public AsyncRabbitTemplate(RabbitTemplate template, AbstractMessageListenerContainer container, - String replyAddress) { + @Nullable String replyAddress) { + Assert.notNull(template, "'template' cannot be null"); Assert.notNull(container, "'container' cannot be null"); this.template = template; this.container = container; this.container.setMessageListener(this); this.directReplyToContainer = null; - if (replyAddress == null) { - this.replyAddress = container.getQueueNames()[0]; - } - else { - this.replyAddress = replyAddress; - } + this.replyAddress = Objects.requireNonNullElseGet(replyAddress, () -> container.getQueueNames()[0]); } /** @@ -221,7 +222,7 @@ public AsyncRabbitTemplate(RabbitTemplate template, AbstractMessageListenerConta * @param routingKey the default routing key. * @since 2.0 */ - public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey) { + public AsyncRabbitTemplate(ConnectionFactory connectionFactory, @Nullable String exchange, String routingKey) { this(new RabbitTemplate(connectionFactory)); Assert.notNull(routingKey, "'routingKey' cannot be null"); this.template.setExchange(exchange == null ? "" : exchange); @@ -234,6 +235,7 @@ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, * @param template a {@link RabbitTemplate} * @since 2.0 */ + @SuppressWarnings("this-escape") public AsyncRabbitTemplate(RabbitTemplate template) { Assert.notNull(template, "'template' cannot be null"); this.template = template; @@ -296,7 +298,7 @@ public void setMandatoryExpressionString(String mandatoryExpression) { /** * Set to true to enable publisher confirms. When enabled, the {@link RabbitFuture} * returned by the send and receive operation will have a - * {@code ListenableFuture} in its {@code confirm} property. + * {@code CompletableFuture} in its {@code confirm} property. * @param enableConfirms true to enable publisher confirms. */ public void setEnableConfirms(boolean enableConfirms) { @@ -339,10 +341,16 @@ public void setReceiveTimeout(long receiveTimeout) { * @param taskScheduler the task scheduler * @see #setReceiveTimeout(long) */ - public synchronized void setTaskScheduler(TaskScheduler taskScheduler) { + public void setTaskScheduler(TaskScheduler taskScheduler) { Assert.notNull(taskScheduler, "'taskScheduler' cannot be null"); - this.internalTaskScheduler = false; - this.taskScheduler = taskScheduler; + this.lock.lock(); + try { + this.internalTaskScheduler = false; + this.taskScheduler = taskScheduler; + } + finally { + this.lock.unlock(); + } } /** @@ -375,17 +383,19 @@ public RabbitMessageFuture sendAndReceive(String routingKey, Message message) { @Override public RabbitMessageFuture sendAndReceive(String exchange, String routingKey, Message message) { String correlationId = getOrSetCorrelationIdAndSetReplyTo(message, null); - RabbitMessageFuture future = new RabbitMessageFuture(correlationId, message); + RabbitMessageFuture future = new RabbitMessageFuture(correlationId, message, this::canceler, + this::timeoutTask); CorrelationData correlationData = null; if (this.enableConfirms) { correlationData = new CorrelationData(correlationId); - future.setConfirm(new SettableListenableFuture<>()); + future.setConfirm(new CompletableFuture<>()); } this.pending.put(correlationId, future); if (this.container != null) { this.template.send(exchange, routingKey, message, correlationData); } else { + Assert.notNull(this.directReplyToContainer, "'directReplyToContainer' cannot be null"); ChannelHolder channelHolder = this.directReplyToContainer.getChannelHolder(); future.setChannelHolder(channelHolder); sendDirect(channelHolder.getChannel(), exchange, routingKey, message, correlationData); @@ -419,18 +429,21 @@ public RabbitConverterFuture convertSendAndReceive(Object object, @Override public RabbitConverterFuture convertSendAndReceive(String routingKey, Object object, MessagePostProcessor messagePostProcessor) { + return convertSendAndReceive(this.template.getExchange(), routingKey, object, messagePostProcessor); } @Override public RabbitConverterFuture convertSendAndReceive(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor) { + @Nullable MessagePostProcessor messagePostProcessor) { + return convertSendAndReceive(exchange, routingKey, object, messagePostProcessor, null); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(Object object, ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(this.template.getExchange(), this.template.getRoutingKey(), object, null, responseType); } @@ -438,54 +451,56 @@ public RabbitConverterFuture convertSendAndReceiveAsType(Object object, @Override public RabbitConverterFuture convertSendAndReceiveAsType(String routingKey, Object object, ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(this.template.getExchange(), routingKey, object, null, responseType); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(exchange, routingKey, object, null, responseType); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(this.template.getExchange(), this.template.getRoutingKey(), object, messagePostProcessor, responseType); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(this.template.getExchange(), routingKey, object, messagePostProcessor, responseType); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + Assert.state(this.template.getMessageConverter() instanceof SmartMessageConverter, "template's message converter must be a SmartMessageConverter"); return convertSendAndReceive(exchange, routingKey, object, messagePostProcessor, responseType); } private RabbitConverterFuture convertSendAndReceive(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { - AsyncCorrelationData correlationData = new AsyncCorrelationData(messagePostProcessor, responseType, + AsyncCorrelationData correlationData = new AsyncCorrelationData<>(messagePostProcessor, responseType, this.enableConfirms); if (this.container != null) { this.template.convertAndSend(exchange, routingKey, object, this.messagePostProcessor, correlationData); } else { MessageConverter converter = this.template.getMessageConverter(); - if (converter == null) { - throw new AmqpIllegalStateException( - "No 'messageConverter' specified. Check configuration of RabbitTemplate."); - } Message message = converter.toMessage(object, new MessageProperties()); this.messagePostProcessor.postProcessMessage(message, correlationData, this.template.nullSafeExchange(exchange), this.template.nullSafeRoutingKey(routingKey)); + @SuppressWarnings("NullAway") // Dataflow analysis limitation ChannelHolder channelHolder = this.directReplyToContainer.getChannelHolder(); correlationData.future.setChannelHolder(channelHolder); sendDirect(channelHolder.getChannel(), exchange, routingKey, message, correlationData); @@ -496,7 +511,8 @@ private RabbitConverterFuture convertSendAndReceive(String exchange, Stri } private void sendDirect(Channel channel, String exchange, String routingKey, Message message, - CorrelationData correlationData) { + @Nullable CorrelationData correlationData) { + message.getMessageProperties().setReplyTo(Address.AMQ_RABBITMQ_REPLY_TO); try { if (channel instanceof PublisherCallbackChannel) { @@ -511,44 +527,57 @@ private void sendDirect(Channel channel, String exchange, String routingKey, Mes } @Override - public synchronized void start() { - if (!this.running) { - if (this.internalTaskScheduler) { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix(getBeanName() == null ? "asyncTemplate-" : (getBeanName() + "-")); - scheduler.afterPropertiesSet(); - this.taskScheduler = scheduler; - } - if (this.container != null) { - this.container.start(); - } - if (this.directReplyToContainer != null) { - this.directReplyToContainer.setTaskScheduler(this.taskScheduler); - this.directReplyToContainer.start(); + public void start() { + this.lock.lock(); + try { + if (!this.running) { + if (this.internalTaskScheduler) { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix(getBeanName() + "-"); + scheduler.afterPropertiesSet(); + this.taskScheduler = scheduler; + } + if (this.container != null) { + this.container.start(); + } + if (this.directReplyToContainer != null) { + this.directReplyToContainer.setTaskScheduler(this.taskScheduler); + this.directReplyToContainer.start(); + } } + this.running = true; + } + finally { + this.lock.unlock(); } - this.running = true; } @Override - public synchronized void stop() { - if (this.running) { - if (this.container != null) { - this.container.stop(); - } - if (this.directReplyToContainer != null) { - this.directReplyToContainer.stop(); - } - for (RabbitFuture future : this.pending.values()) { - future.setNackCause("AsyncRabbitTemplate was stopped while waiting for reply"); - future.cancel(true); - } - if (this.internalTaskScheduler) { - ((ThreadPoolTaskScheduler) this.taskScheduler).destroy(); - this.taskScheduler = null; + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public void stop() { + this.lock.lock(); + try { + if (this.running) { + if (this.container != null) { + this.container.stop(); + } + if (this.directReplyToContainer != null) { + this.directReplyToContainer.stop(); + } + for (RabbitFuture future : this.pending.values()) { + future.setNackCause("AsyncRabbitTemplate was stopped while waiting for reply"); + future.cancel(true); + } + if (this.internalTaskScheduler) { + ((ThreadPoolTaskScheduler) this.taskScheduler).destroy(); + this.taskScheduler = null; + } } + this.running = false; + } + finally { + this.lock.unlock(); } - this.running = false; } @Override @@ -568,34 +597,37 @@ public boolean isAutoStartup() { @SuppressWarnings("unchecked") @Override - public void onMessage(Message message, Channel channel) { + public void onMessage(Message message, @Nullable Channel channel) { MessageProperties messageProperties = message.getMessageProperties(); - if (messageProperties != null) { - String correlationId = messageProperties.getCorrelationId(); - if (StringUtils.hasText(correlationId)) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("onMessage: " + message); - } - RabbitFuture future = this.pending.remove(correlationId); - if (future != null) { - if (future instanceof AsyncRabbitTemplate.RabbitConverterFuture) { - MessageConverter messageConverter = this.template.getMessageConverter(); - RabbitConverterFuture rabbitFuture = (RabbitConverterFuture) future; + String correlationId = messageProperties.getCorrelationId(); + if (StringUtils.hasText(correlationId)) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("onMessage: " + message); + } + RabbitFuture future = this.pending.remove(correlationId); + if (future != null) { + if (future instanceof RabbitConverterFuture) { + MessageConverter messageConverter = this.template.getMessageConverter(); + RabbitConverterFuture rabbitFuture = (RabbitConverterFuture) future; + try { Object converted = rabbitFuture.getReturnType() != null - && messageConverter instanceof SmartMessageConverter - ? ((SmartMessageConverter) messageConverter).fromMessage(message, + && messageConverter instanceof SmartMessageConverter smart + ? smart.fromMessage(message, rabbitFuture.getReturnType()) : messageConverter.fromMessage(message); - rabbitFuture.set(converted); + rabbitFuture.complete(converted); } - else { - ((RabbitMessageFuture) future).set(message); + catch (MessageConversionException e) { + rabbitFuture.completeExceptionally(e); } } else { - if (this.logger.isWarnEnabled()) { - this.logger.warn("No pending reply - perhaps timed out: " + message); - } + ((RabbitMessageFuture) future).complete(message); + } + } + else { + if (this.logger.isWarnEnabled()) { + this.logger.warn("No pending reply - perhaps timed out: " + message); } } } @@ -608,7 +640,7 @@ public void returnedMessage(ReturnedMessage returned) { if (StringUtils.hasText(correlationId)) { RabbitFuture future = this.pending.remove(correlationId); if (future != null) { - future.setException(new AmqpMessageReturnedException("Message returned", returned)); + future.completeExceptionally(new AmqpMessageReturnedException("Message returned", returned)); } else { if (this.logger.isWarnEnabled()) { @@ -620,24 +652,23 @@ public void returnedMessage(ReturnedMessage returned) { } @Override - public void confirm(@NonNull CorrelationData correlationData, boolean ack, @Nullable String cause) { + public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause) { if (this.logger.isDebugEnabled()) { this.logger.debug("Confirm: " + correlationData + ", ack=" + ack + (cause == null ? "" : (", cause: " + cause))); } + Assert.notNull(correlationData, "'correlationData' must not be null"); String correlationId = correlationData.getId(); - if (correlationId != null) { - RabbitFuture future = this.pending.get(correlationId); - if (future != null) { - future.setNackCause(cause); - ((SettableListenableFuture) future.getConfirm()).set(ack); - } - else { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Confirm: " + correlationData + ", ack=" + ack - + (cause == null ? "" : (", cause: " + cause)) - + " no pending future - either canceled or the reply is already received"); - } + RabbitFuture future = this.pending.get(correlationId); + if (future != null) { + future.setNackCause(cause); + future.getConfirm().complete(ack); + } + else { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Confirm: " + correlationData + ", ack=" + ack + + (cause == null ? "" : (", cause: " + cause)) + + " no pending future - either canceled or the reply is already received"); } } } @@ -645,161 +676,48 @@ public void confirm(@NonNull CorrelationData correlationData, boolean ack, @Null private String getOrSetCorrelationIdAndSetReplyTo(Message message, @Nullable AsyncCorrelationData correlationData) { - String correlationId; MessageProperties messageProperties = message.getMessageProperties(); Assert.notNull(messageProperties, "the message properties cannot be null"); - String currentCorrelationId = messageProperties.getCorrelationId(); - if (!StringUtils.hasText(currentCorrelationId)) { + String correlationId = messageProperties.getCorrelationId(); + if (!StringUtils.hasText(correlationId)) { correlationId = correlationData != null ? correlationData.getId() : UUID.randomUUID().toString(); messageProperties.setCorrelationId(correlationId); Assert.isNull(messageProperties.getReplyTo(), "'replyTo' property must be null"); } - else { - correlationId = currentCorrelationId; - } messageProperties.setReplyTo(this.replyAddress); return correlationId; } - @Override - public String toString() { - return this.beanName == null ? super.toString() : (this.getClass().getSimpleName() + ": " + this.beanName); - } - - /** - * Base class for {@link ListenableFuture}s returned by {@link AsyncRabbitTemplate}. - * @param the type. - * @since 1.6 - */ - public abstract class RabbitFuture extends SettableListenableFuture { - - private final String correlationId; - - private final Message requestMessage; - - private ScheduledFuture timeoutTask; - - private volatile ListenableFuture confirm; - - private String nackCause; - - private ChannelHolder channelHolder; - - public RabbitFuture(String correlationId, Message requestMessage) { - this.correlationId = correlationId; - this.requestMessage = requestMessage; - } - - void setChannelHolder(ChannelHolder channel) { - this.channelHolder = channel; - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - if (this.timeoutTask != null) { - this.timeoutTask.cancel(true); - } - AsyncRabbitTemplate.this.pending.remove(this.correlationId); - if (this.channelHolder != null && AsyncRabbitTemplate.this.directReplyToContainer != null) { - AsyncRabbitTemplate.this.directReplyToContainer - .releaseConsumerFor(this.channelHolder, false, null); // NOSONAR - } - return super.cancel(mayInterruptIfRunning); - } - - /** - * When confirms are enabled contains a {@link ListenableFuture} - * for the confirmation. - * @return the future. - */ - public ListenableFuture getConfirm() { - return this.confirm; - } - - void setConfirm(ListenableFuture confirm) { - this.confirm = confirm; - } - - /** - * When confirms are enabled and a nack is received, contains - * the cause for the nack, if any. - * @return the cause. - */ - public String getNackCause() { - return this.nackCause; - } - - void setNackCause(String nackCause) { - this.nackCause = nackCause; + private void canceler(String correlationId, @Nullable ChannelHolder channelHolder) { + this.pending.remove(correlationId); + if (channelHolder != null && this.directReplyToContainer != null) { + this.directReplyToContainer + .releaseConsumerFor(channelHolder, false, null); // NOSONAR } + } - void startTimer() { - if (AsyncRabbitTemplate.this.receiveTimeout > 0) { - synchronized (AsyncRabbitTemplate.this) { - if (!AsyncRabbitTemplate.this.running) { - AsyncRabbitTemplate.this.pending.remove(this.correlationId); - throw new IllegalStateException("'AsyncRabbitTemplate' must be started."); - } - this.timeoutTask = AsyncRabbitTemplate.this.taskScheduler.schedule(new TimeoutTask(), - new Date(System.currentTimeMillis() + AsyncRabbitTemplate.this.receiveTimeout)); + private @Nullable ScheduledFuture timeoutTask(RabbitFuture future) { + if (this.receiveTimeout > 0) { + this.lock.lock(); + try { + if (!this.running) { + this.pending.remove(future.getCorrelationId()); + throw new IllegalStateException("'AsyncRabbitTemplate' must be started."); } + return this.taskScheduler.schedule( + new TimeoutTask(future, this.pending, this.directReplyToContainer), + Instant.now().plusMillis(this.receiveTimeout)); } - else { - this.timeoutTask = null; - } - } - - private class TimeoutTask implements Runnable { - - @Override - public void run() { - AsyncRabbitTemplate.this.pending.remove(RabbitFuture.this.correlationId); - if (RabbitFuture.this.channelHolder != null - && AsyncRabbitTemplate.this.directReplyToContainer != null) { - AsyncRabbitTemplate.this.directReplyToContainer - .releaseConsumerFor(RabbitFuture.this.channelHolder, false, null); // NOSONAR - } - setException(new AmqpReplyTimeoutException("Reply timed out", RabbitFuture.this.requestMessage)); + finally { + this.lock.unlock(); } - - } - - } - - /** - * A {@link RabbitFuture} with a return type of {@link Message}. - * @since 1.6 - */ - public class RabbitMessageFuture extends RabbitFuture { - - public RabbitMessageFuture(String correlationId, Message requestMessage) { - super(correlationId, requestMessage); } - + return null; } - /** - * A {@link RabbitFuture} with a return type of the template's - * generic parameter. - * @param the type. - * @since 1.6 - */ - public class RabbitConverterFuture extends RabbitFuture { - - private volatile ParameterizedTypeReference returnType; - - public RabbitConverterFuture(String correlationId, Message requestMessage) { - super(correlationId, requestMessage); - } - - public ParameterizedTypeReference getReturnType() { - return this.returnType; - } - - public void setReturnType(ParameterizedTypeReference returnType) { - this.returnType = returnType; - } - + @Override + public String toString() { + return this.getClass().getSimpleName() + ": " + this.beanName; } private final class CorrelationMessagePostProcessor implements MessagePostProcessor { @@ -814,17 +732,19 @@ public Message postProcessMessage(Message message) throws AmqpException { @SuppressWarnings("unchecked") @Override - public Message postProcessMessage(Message message, Correlation correlation) throws AmqpException { + public Message postProcessMessage(Message message, @Nullable Correlation correlation) throws AmqpException { Message messageToSend = message; AsyncCorrelationData correlationData = (AsyncCorrelationData) correlation; + Assert.notNull(correlationData, "correlationData cannot be null"); if (correlationData.userPostProcessor != null) { messageToSend = correlationData.userPostProcessor.postProcessMessage(message); } String correlationId = getOrSetCorrelationIdAndSetReplyTo(messageToSend, correlationData); - correlationData.future = new RabbitConverterFuture(correlationId, message); + correlationData.future = new RabbitConverterFuture<>(correlationId, message, + AsyncRabbitTemplate.this::canceler, AsyncRabbitTemplate.this::timeoutTask); if (correlationData.enableConfirms) { correlationData.setId(correlationId); - correlationData.future.setConfirm(new SettableListenableFuture<>()); + correlationData.future.setConfirm(new CompletableFuture<>()); } correlationData.future.setReturnType(correlationData.returnType); AsyncRabbitTemplate.this.pending.put(correlationId, correlationData.future); @@ -835,16 +755,17 @@ public Message postProcessMessage(Message message, Correlation correlation) thro private static class AsyncCorrelationData extends CorrelationData { - final MessagePostProcessor userPostProcessor; // NOSONAR + final @Nullable MessagePostProcessor userPostProcessor; // NOSONAR - final ParameterizedTypeReference returnType; // NOSONAR + final @Nullable ParameterizedTypeReference returnType; // NOSONAR final boolean enableConfirms; // NOSONAR + @SuppressWarnings("NullAway.Init") volatile RabbitConverterFuture future; // NOSONAR - AsyncCorrelationData(MessagePostProcessor userPostProcessor, ParameterizedTypeReference returnType, - boolean enableConfirms) { + AsyncCorrelationData(@Nullable MessagePostProcessor userPostProcessor, + @Nullable ParameterizedTypeReference returnType, boolean enableConfirms) { this.userPostProcessor = userPostProcessor; this.returnType = returnType; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java new file mode 100644 index 0000000000..7e10cf519a --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit; + +import java.util.concurrent.ScheduledFuture; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; +import org.springframework.core.ParameterizedTypeReference; + +/** + * A {@link RabbitFuture} with a return type of the template's + * generic parameter. + * @param the type. + * + * @author Gary Russell + * @since 2.4.7 + */ +public class RabbitConverterFuture extends RabbitFuture { + + private volatile @Nullable ParameterizedTypeReference returnType; + + RabbitConverterFuture(String correlationId, Message requestMessage, + BiConsumer canceler, + Function, @Nullable ScheduledFuture> timeoutTaskFunction) { + + super(correlationId, requestMessage, canceler, timeoutTaskFunction); + } + + public @Nullable ParameterizedTypeReference getReturnType() { + return this.returnType; + } + + public void setReturnType(@Nullable ParameterizedTypeReference returnType) { + this.returnType = returnType; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java new file mode 100644 index 0000000000..2c32b730f9 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java @@ -0,0 +1,151 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; + +/** + * Base class for {@link CompletableFuture}s returned by {@link AsyncRabbitTemplate}. + * @param the type. + * + * @author Gary Russell + * @author Artem Bilan + * + * @since 2.4.7 + */ +public abstract class RabbitFuture extends CompletableFuture { + + private final String correlationId; + + private final Message requestMessage; + + private final BiConsumer canceler; + + private final Function, @Nullable ScheduledFuture> timeoutTaskFunction; + + private @Nullable ScheduledFuture timeoutTask; + + @SuppressWarnings("NullAway.Init") + private volatile CompletableFuture confirm; + + private @Nullable String nackCause; + + private @Nullable ChannelHolder channelHolder; + + protected RabbitFuture(String correlationId, Message requestMessage, + BiConsumer canceler, + Function, @Nullable ScheduledFuture> timeoutTaskFunction) { + + this.correlationId = correlationId; + this.requestMessage = requestMessage; + this.canceler = canceler; + this.timeoutTaskFunction = timeoutTaskFunction; + } + + void setChannelHolder(ChannelHolder channel) { + this.channelHolder = channel; + } + + String getCorrelationId() { + return this.correlationId; + } + + @Nullable + ChannelHolder getChannelHolder() { + return this.channelHolder; + } + + Message getRequestMessage() { + return this.requestMessage; + } + + @Override + public boolean complete(T value) { + try { + return super.complete(value); + } + finally { + cancelTimeoutTaskIfAny(); + } + } + + @Override + public boolean completeExceptionally(Throwable ex) { + try { + return super.completeExceptionally(ex); + } + finally { + cancelTimeoutTaskIfAny(); + } + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + this.canceler.accept(this.correlationId, this.channelHolder); + try { + return super.cancel(mayInterruptIfRunning); + } + finally { + cancelTimeoutTaskIfAny(); + } + } + + private void cancelTimeoutTaskIfAny() { + if (this.timeoutTask != null) { + this.timeoutTask.cancel(true); + } + } + + /** + * When confirms are enabled contains a {@link CompletableFuture} + * for the confirmation. + * @return the future. + */ + public CompletableFuture getConfirm() { + return this.confirm; + } + + void setConfirm(CompletableFuture confirm) { + this.confirm = confirm; + } + + /** + * When confirms are enabled and a nack is received, contains + * the cause for the nack, if any. + * @return the cause. + */ + public @Nullable String getNackCause() { + return this.nackCause; + } + + void setNackCause(@Nullable String nackCause) { + this.nackCause = nackCause; + } + + void startTimer() { + this.timeoutTask = this.timeoutTaskFunction.apply(this); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java new file mode 100644 index 0000000000..33b8367998 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit; + +import java.util.concurrent.ScheduledFuture; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; + +/** + * A {@link RabbitFuture} with a return type of {@link Message}. + * + * @author Gary Russell + * @since 2.4.7 + */ +public class RabbitMessageFuture extends RabbitFuture { + + RabbitMessageFuture(String correlationId, Message requestMessage, + BiConsumer canceler, + Function, @Nullable ScheduledFuture> timeoutTaskFunction) { + + super(correlationId, requestMessage, canceler, timeoutTaskFunction); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java new file mode 100644 index 0000000000..b1d2e1e87f --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit; + +import java.util.concurrent.ConcurrentMap; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.AmqpReplyTimeoutException; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; + +/** + * A {@link Runnable} used to time out a {@link RabbitFuture}. + * + * @author Gary Russell + * @since 2.4.7 + */ +public class TimeoutTask implements Runnable { + + private final RabbitFuture future; + + private final ConcurrentMap> pending; + + private final @Nullable DirectReplyToMessageListenerContainer container; + + TimeoutTask(RabbitFuture future, ConcurrentMap> pending, + @Nullable DirectReplyToMessageListenerContainer container) { + + this.future = future; + this.pending = pending; + this.container = container; + } + + @Override + public void run() { + this.pending.remove(this.future.getCorrelationId()); + ChannelHolder holder = this.future.getChannelHolder(); + if (holder != null && this.container != null) { + this.container.releaseConsumerFor(holder, false, null); // NOSONAR + } + this.future.completeExceptionally( + new AmqpReplyTimeoutException("Reply timed out", this.future.getRequestMessage())); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java index f7724f48d4..a83d043419 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java @@ -104,7 +104,7 @@ *

Annotated methods can use flexible signature; in particular, it is possible to use * the {@link org.springframework.messaging.Message Message} abstraction and related annotations, * see {@link RabbitListener} Javadoc for more details. For instance, the following would - * inject the content of the message and a a custom "myCounter" AMQP header: + * inject the content of the message and a custom "myCounter" AMQP header: * *

  * @RabbitListener(containerFactory = "myRabbitListenerContainerFactory", queues = "myQueue")
@@ -175,7 +175,7 @@
  *     @Override
  *     public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
  *         registrar.setEndpointRegistry(myRabbitListenerEndpointRegistry());
- *         registrar.setMessageHandlerMethodFactory(myMessageHandlerMethodFactory);
+ *         registrar.setMessageHandlerMethodFactory(myMessageHandlerMethodFactory());
  *     }
  *
  *     @Bean
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfiguration.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfiguration.java
index cfc127cb16..cdd1c63b4d 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfiguration.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
 
 package org.springframework.amqp.rabbit.annotation;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils;
 import org.springframework.beans.factory.support.BeanDefinitionRegistry;
 import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -23,7 +25,6 @@
 import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
 import org.springframework.core.env.Environment;
 import org.springframework.core.type.AnnotationMetadata;
-import org.springframework.lang.Nullable;
 
 /**
  * An {@link ImportBeanDefinitionRegistrar} class that registers
@@ -31,6 +32,7 @@
  * is enabled.
  *
  * @author Wander Costa
+ * @author Artem Bilan
  *
  * @since 1.4
  *
@@ -41,6 +43,7 @@
  */
 public class MultiRabbitBootstrapConfiguration implements ImportBeanDefinitionRegistrar, EnvironmentAware {
 
+	@SuppressWarnings("NullAway.Init")
 	private Environment environment;
 
 	@Override
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java
index 939bd6b583..3a99b98931 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,6 +24,8 @@
 
 import org.springframework.amqp.core.Declarable;
 import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils;
+import org.springframework.amqp.rabbit.core.RabbitAdmin;
+import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
 import org.springframework.util.StringUtils;
 
 /**
@@ -35,6 +37,7 @@
  * configuration, preventing the server from automatic binding non-related structures.
  *
  * @author Wander Costa
+ * @author Ngoc Nhan
  *
  * @since 2.3
  */
@@ -59,7 +62,7 @@ private RabbitListener proxyIfAdminNotPresent(final RabbitListener rabbitListene
 			return rabbitListener;
 		}
 		return (RabbitListener) Proxy.newProxyInstance(
-				RabbitListener.class.getClassLoader(), new Class[]{RabbitListener.class},
+				RabbitListener.class.getClassLoader(), new Class[] {RabbitListener.class},
 				new RabbitListenerAdminReplacementInvocationHandler(rabbitListener, rabbitAdmin));
 	}
 
@@ -70,28 +73,39 @@ private RabbitListener proxyIfAdminNotPresent(final RabbitListener rabbitListene
 	 * @return The name of the RabbitAdmin bean.
 	 */
 	protected String resolveMultiRabbitAdminName(RabbitListener rabbitListener) {
-		String admin = super.resolveExpressionAsString(rabbitListener.admin(), "admin");
-		if (!StringUtils.hasText(admin) && StringUtils.hasText(rabbitListener.containerFactory())) {
-			admin = rabbitListener.containerFactory() + RabbitListenerConfigUtils.MULTI_RABBIT_ADMIN_SUFFIX;
+
+		var admin = rabbitListener.admin();
+		if (StringUtils.hasText(admin)) {
+
+			var resolved = super.resolveExpression(admin);
+			if (resolved instanceof RabbitAdmin rabbitAdmin) {
+
+				return rabbitAdmin.getBeanName();
+			}
+
+			return super.resolveExpressionAsString(admin, "admin");
 		}
-		if (!StringUtils.hasText(admin)) {
-			admin = RabbitListenerConfigUtils.RABBIT_ADMIN_BEAN_NAME;
+
+		var containerFactory = rabbitListener.containerFactory();
+		if (StringUtils.hasText(containerFactory)) {
+
+			var resolved = super.resolveExpression(containerFactory);
+			if (resolved instanceof RabbitListenerContainerFactory rlcf) {
+
+				return rlcf.getBeanName() + RabbitListenerConfigUtils.MULTI_RABBIT_ADMIN_SUFFIX;
+			}
+
+			return resolved + RabbitListenerConfigUtils.MULTI_RABBIT_ADMIN_SUFFIX;
 		}
-		return admin;
+
+		return RabbitListenerConfigUtils.RABBIT_ADMIN_BEAN_NAME;
 	}
 
 	/**
 	 * An {@link InvocationHandler} to provide a replacing admin() parameter of the listener.
 	 */
-	private static final class RabbitListenerAdminReplacementInvocationHandler implements InvocationHandler {
-
-		private final RabbitListener target;
-		private final String admin;
-
-		private RabbitListenerAdminReplacementInvocationHandler(final RabbitListener target, final String admin) {
-			this.target = target;
-			this.admin = admin;
-		}
+	private record RabbitListenerAdminReplacementInvocationHandler(RabbitListener target,
+																   String admin) implements InvocationHandler {
 
 		@Override
 		public Object invoke(final Object proxy, final Method method, final Object[] args)
@@ -101,6 +115,7 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg
 			}
 			return method.invoke(this.target, args);
 		}
+
 	}
 
 }
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitBootstrapConfiguration.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitBootstrapConfiguration.java
index 9789a3b0d9..3bbcef142a 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitBootstrapConfiguration.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitBootstrapConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,13 +16,14 @@
 
 package org.springframework.amqp.rabbit.annotation;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils;
 import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
 import org.springframework.beans.factory.support.BeanDefinitionRegistry;
 import org.springframework.beans.factory.support.RootBeanDefinition;
 import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
 import org.springframework.core.type.AnnotationMetadata;
-import org.springframework.lang.Nullable;
 
 /**
  * An {@link ImportBeanDefinitionRegistrar} class that registers
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java
index 59a0cd9941..8b3787075e 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014-2020 the original author or authors.
+ * Copyright 2014-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.
@@ -148,6 +148,8 @@
 	 * application context, the queue will be declared on the broker with default
 	 * binding (default exchange with the queue name as the routing key).
 	 * Mutually exclusive with {@link #bindings()} and {@link #queues()}.
+	 * NOTE: Broker-named queues cannot be declared this way, they must be defined
+	 * as beans (with an empty string for the name).
 	 * @return the queue(s) to declare.
 	 * @see org.springframework.amqp.rabbit.listener.MessageListenerContainer
 	 * @since 2.0
@@ -186,6 +188,8 @@
 	 * Array of {@link QueueBinding}s providing the listener's queue names, together
 	 * with the exchange and optional binding information.
 	 * Mutually exclusive with {@link #queues()} and {@link #queuesToDeclare()}.
+	 * NOTE: Broker-named queues cannot be declared this way, they must be defined
+	 * as beans (with an empty string for the name).
 	 * @return the bindings.
 	 * @see org.springframework.amqp.rabbit.listener.MessageListenerContainer
 	 * @since 1.5
@@ -328,4 +332,18 @@
 	 */
 	String converterWinsContentType() default "true";
 
+	/**
+	 * Override the container factory's {@code batchListener} property. The listener
+	 * method signature should receive a {@code List}; refer to the reference
+	 * documentation. This allows a single container factory to be used for both record
+	 * and batch listeners; previously separate container factories were required.
+	 * @return "true" for the annotated method to be a batch listener or "false" for a
+	 * single message listener. If not set, the container factory setting is used. SpEL and
+	 * property place holders are not supported because the listener type cannot be
+	 * variable.
+	 * @since 3.0
+	 * @see Boolean#parseBoolean(String)
+	 */
+	String batch() default "";
+
 }
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java
index dc6e5f4fd6..60f71ecf67 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014-2021 the original author or authors.
+ * Copyright 2014-2025 the original author 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.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
 
 import org.springframework.amqp.core.AcknowledgeMode;
 import org.springframework.amqp.core.AmqpAdmin;
@@ -53,6 +54,7 @@
 import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
 import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
 import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
+import org.springframework.amqp.rabbit.listener.adapter.AmqpMessageHandlerMethodFactory;
 import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor;
 import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler;
 import org.springframework.amqp.support.converter.MessageConverter;
@@ -75,6 +77,7 @@
 import org.springframework.context.expression.StandardBeanExpressionResolver;
 import org.springframework.core.Ordered;
 import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.MergedAnnotation;
 import org.springframework.core.annotation.MergedAnnotations;
 import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
 import org.springframework.core.convert.ConversionService;
@@ -82,7 +85,8 @@
 import org.springframework.core.convert.support.DefaultConversionService;
 import org.springframework.core.env.Environment;
 import org.springframework.core.task.TaskExecutor;
-import org.springframework.lang.Nullable;
+import org.springframework.format.support.DefaultFormattingConversionService;
+import org.springframework.messaging.converter.GenericMessageConverter;
 import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
 import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
 import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
@@ -108,7 +112,7 @@
  *
  * 

Auto-detect any {@link RabbitListenerConfigurer} instances in the container, * allowing for customization of the registry to be used, the default container - * factory or for fine-grained control over endpoints registration. See + * factory or for fine-grained control over endpoint registrations. See * {@link EnableRabbit} Javadoc for complete usage details. * * @author Stephane Nicoll @@ -116,6 +120,7 @@ * @author Gary Russell * @author Alex Panchenko * @author Artem Bilan + * @author Ngoc Nhan * * @since 1.4 * @@ -144,12 +149,14 @@ public class RabbitListenerAnnotationBeanPostProcessor private final Set emptyStringArguments = new HashSet<>(); - private RabbitListenerEndpointRegistry endpointRegistry; + private @Nullable RabbitListenerEndpointRegistry endpointRegistry; private String defaultContainerFactoryBeanName = DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME; + @SuppressWarnings("NullAway.Init") private BeanFactory beanFactory; + @SuppressWarnings("NullAway.Init") private ClassLoader beanClassLoader; private final RabbitHandlerMethodFactoryAdapter messageHandlerMethodFactory = @@ -163,6 +170,7 @@ public class RabbitListenerAnnotationBeanPostProcessor private BeanExpressionResolver resolver = new StandardBeanExpressionResolver(); + @SuppressWarnings("NullAway.Init") private BeanExpressionContext expressionContext; private int increment; @@ -218,9 +226,12 @@ public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory messageHa @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; - if (beanFactory instanceof ConfigurableListableBeanFactory) { - this.resolver = ((ConfigurableListableBeanFactory) beanFactory).getBeanExpressionResolver(); - this.expressionContext = new BeanExpressionContext((ConfigurableListableBeanFactory) beanFactory, null); + if (beanFactory instanceof ConfigurableListableBeanFactory clbf) { + BeanExpressionResolver beanExpressionResolver = clbf.getBeanExpressionResolver(); + if (beanExpressionResolver != null) { + this.resolver = beanExpressionResolver; + } + this.expressionContext = new BeanExpressionContext(clbf, null); } } @@ -254,9 +265,9 @@ MessageHandlerMethodFactory getMessageHandlerMethodFactory() { public void afterSingletonsInstantiated() { this.registrar.setBeanFactory(this.beanFactory); - if (this.beanFactory instanceof ListableBeanFactory) { + if (this.beanFactory instanceof ListableBeanFactory lbf) { Map instances = - ((ListableBeanFactory) this.beanFactory).getBeansOfType(RabbitListenerConfigurer.class); + lbf.getBeansOfType(RabbitListenerConfigurer.class); for (RabbitListenerConfigurer configurer : instances.values()) { configurer.configureRabbitListeners(this.registrar); } @@ -264,8 +275,6 @@ public void afterSingletonsInstantiated() { if (this.registrar.getEndpointRegistry() == null) { if (this.endpointRegistry == null) { - Assert.state(this.beanFactory != null, - "BeanFactory must be set to find endpoint registry by bean name"); this.endpointRegistry = this.beanFactory.getBean( RabbitListenerConfigUtils.RABBIT_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME, RabbitListenerEndpointRegistry.class); @@ -273,9 +282,7 @@ public void afterSingletonsInstantiated() { this.registrar.setEndpointRegistry(this.endpointRegistry); } - if (this.defaultContainerFactoryBeanName != null) { - this.registrar.setContainerFactoryBeanName(this.defaultContainerFactoryBeanName); - } + this.registrar.setContainerFactoryBeanName(this.defaultContainerFactoryBeanName); // Set the custom handler method factory once resolved by the configurer MessageHandlerMethodFactory handlerMethodFactory = this.registrar.getMessageHandlerMethodFactory(); @@ -290,12 +297,6 @@ public void afterSingletonsInstantiated() { this.typeCache.clear(); } - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - return bean; - } - @Override public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException { Class targetClass = AopUtils.getTargetClass(bean); @@ -312,15 +313,15 @@ public Object postProcessAfterInitialization(final Object bean, final String bea } private TypeMetadata buildMetadata(Class targetClass) { - Collection classLevelListeners = findListenerAnnotations(targetClass); - final boolean hasClassLevelListeners = classLevelListeners.size() > 0; + List classLevelListeners = findListenerAnnotations(targetClass); + final boolean hasClassLevelListeners = !classLevelListeners.isEmpty(); final List methods = new ArrayList<>(); final List multiMethods = new ArrayList<>(); ReflectionUtils.doWithMethods(targetClass, method -> { - Collection listenerAnnotations = findListenerAnnotations(method); - if (listenerAnnotations.size() > 0) { + List listenerAnnotations = findListenerAnnotations(method); + if (!listenerAnnotations.isEmpty()) { methods.add(new ListenerMethod(method, - listenerAnnotations.toArray(new RabbitListener[listenerAnnotations.size()]))); + listenerAnnotations.toArray(new RabbitListener[0]))); } if (hasClassLevelListeners) { RabbitHandler rabbitHandler = AnnotationUtils.findAnnotation(method, RabbitHandler.class); @@ -328,34 +329,47 @@ private TypeMetadata buildMetadata(Class targetClass) { multiMethods.add(method); } } - }, ReflectionUtils.USER_DECLARED_METHODS); + }, ReflectionUtils.USER_DECLARED_METHODS + .and(meth -> !meth.getDeclaringClass().getName().contains("$MockitoMock$"))); if (methods.isEmpty() && multiMethods.isEmpty()) { return TypeMetadata.EMPTY; } return new TypeMetadata( - methods.toArray(new ListenerMethod[methods.size()]), - multiMethods.toArray(new Method[multiMethods.size()]), - classLevelListeners.toArray(new RabbitListener[classLevelListeners.size()])); + methods.toArray(new ListenerMethod[0]), + multiMethods.toArray(new Method[0]), + classLevelListeners.toArray(new RabbitListener[0])); } - private Collection findListenerAnnotations(AnnotatedElement element) { + private List findListenerAnnotations(AnnotatedElement element) { return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY) .stream(RabbitListener.class) - .map(ann -> ann.synthesize()) + .filter(tma -> { + Object source = tma.getSource(); + String name = ""; + if (source instanceof Class clazz) { + name = clazz.getName(); + } + else if (source instanceof Method method) { + name = method.getDeclaringClass().getName(); + } + return !name.contains("$MockitoMock$"); + }) + .map(MergedAnnotation::synthesize) .collect(Collectors.toList()); } private void processMultiMethodListeners(RabbitListener[] classLevelListeners, Method[] multiMethods, Object bean, String beanName) { - List checkedMethods = new ArrayList(); + List checkedMethods = new ArrayList<>(); Method defaultMethod = null; for (Method method : multiMethods) { Method checked = checkProxy(method, bean); - if (AnnotationUtils.findAnnotation(method, RabbitHandler.class).isDefault()) { // NOSONAR never null + RabbitHandler annotation = AnnotationUtils.findAnnotation(method, RabbitHandler.class); + if (annotation != null && annotation.isDefault()) { final Method toAssert = defaultMethod; Assert.state(toAssert == null, () -> "Only one @RabbitHandler can be marked 'isDefault', found: " - + toAssert.toString() + " and " + method.toString()); + + toAssert + " and " + method); defaultMethod = checked; } checkedMethods.add(checked); @@ -389,6 +403,7 @@ private Method checkProxy(Method methodArg, Object bean) { break; } catch (@SuppressWarnings("unused") NoSuchMethodException noMethod) { + // Ignore } } } @@ -414,7 +429,15 @@ protected Collection processListener(MethodRabbitListenerEndpoint en endpoint.setBean(bean); endpoint.setMessageHandlerMethodFactory(this.messageHandlerMethodFactory); endpoint.setId(getEndpointId(rabbitListener)); - endpoint.setQueueNames(resolveQueues(rabbitListener, declarables)); + List resolvedQueues = resolveQueues(rabbitListener, declarables); + if (!resolvedQueues.isEmpty()) { + if (resolvedQueues.get(0) instanceof String) { + endpoint.setQueueNames(resolvedQueues.stream().map(o -> (String) o).toArray(String[]::new)); + } + else { + endpoint.setQueues(resolvedQueues.stream().map(o -> (Queue) o).toArray(Queue[]::new)); + } + } endpoint.setConcurrency(resolveExpressionAsStringOrInteger(rabbitListener.concurrency(), "concurrency")); endpoint.setBeanFactory(this.beanFactory); endpoint.setReturnExceptions(resolveExpressionAsBoolean(rabbitListener.returnExceptions())); @@ -422,8 +445,8 @@ protected Collection processListener(MethodRabbitListenerEndpoint en String group = rabbitListener.group(); if (StringUtils.hasText(group)) { Object resolvedGroup = resolveExpression(group); - if (resolvedGroup instanceof String) { - endpoint.setGroup((String) resolvedGroup); + if (resolvedGroup instanceof String str) { + endpoint.setGroup(str); } } String autoStartup = rabbitListener.autoStartup(); @@ -449,6 +472,9 @@ protected Collection processListener(MethodRabbitListenerEndpoint en resolvePostProcessor(endpoint, rabbitListener, target, beanName); resolveMessageConverter(endpoint, rabbitListener, target, beanName); resolveReplyContentType(endpoint, rabbitListener); + if (StringUtils.hasText(rabbitListener.batch())) { + endpoint.setBatchListener(Boolean.parseBoolean(rabbitListener.batch())); + } RabbitListenerContainerFactory factory = resolveContainerFactory(rabbitListener, target, beanName); this.registrar.registerEndpoint(endpoint, factory); @@ -457,8 +483,8 @@ protected Collection processListener(MethodRabbitListenerEndpoint en private void resolveErrorHandler(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener) { Object errorHandler = resolveExpression(rabbitListener.errorHandler()); - if (errorHandler instanceof RabbitListenerErrorHandler) { - endpoint.setErrorHandler((RabbitListenerErrorHandler) errorHandler); + if (errorHandler instanceof RabbitListenerErrorHandler rleh) { + endpoint.setErrorHandler(rleh); } else { String errorHandlerBeanName = resolveExpressionAsString(rabbitListener.errorHandler(), "errorHandler"); @@ -473,11 +499,11 @@ private void resolveAckMode(MethodRabbitListenerEndpoint endpoint, RabbitListene String ackModeAttr = rabbitListener.ackMode(); if (StringUtils.hasText(ackModeAttr)) { Object ackMode = resolveExpression(ackModeAttr); - if (ackMode instanceof String) { - endpoint.setAckMode(AcknowledgeMode.valueOf((String) ackMode)); + if (ackMode instanceof String str) { + endpoint.setAckMode(AcknowledgeMode.valueOf(str)); } - else if (ackMode instanceof AcknowledgeMode) { - endpoint.setAckMode((AcknowledgeMode) ackMode); + else if (ackMode instanceof AcknowledgeMode mode) { + endpoint.setAckMode(mode); } else { Assert.isNull(ackMode, "ackMode must resolve to a String or AcknowledgeMode"); @@ -487,13 +513,12 @@ else if (ackMode instanceof AcknowledgeMode) { private void resolveAdmin(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener, Object adminTarget) { Object resolved = resolveExpression(rabbitListener.admin()); - if (resolved instanceof AmqpAdmin) { - endpoint.setAdmin((AmqpAdmin) resolved); + if (resolved instanceof AmqpAdmin admin) { + endpoint.setAdmin(admin); } else { String rabbitAdmin = resolveExpressionAsString(rabbitListener.admin(), "admin"); if (StringUtils.hasText(rabbitAdmin)) { - Assert.state(this.beanFactory != null, "BeanFactory must be set to resolve RabbitAdmin by bean name"); try { endpoint.setAdmin(this.beanFactory.getBean(rabbitAdmin, RabbitAdmin.class)); } @@ -512,13 +537,12 @@ private RabbitListenerContainerFactory resolveContainerFactory(RabbitListener RabbitListenerContainerFactory factory = null; Object resolved = resolveExpression(rabbitListener.containerFactory()); - if (resolved instanceof RabbitListenerContainerFactory) { - return (RabbitListenerContainerFactory) resolved; + if (resolved instanceof RabbitListenerContainerFactory rlcf) { + return rlcf; } String containerFactoryBeanName = resolveExpressionAsString(rabbitListener.containerFactory(), "containerFactory"); if (StringUtils.hasText(containerFactoryBeanName)) { - assertBeanFactory(); try { factory = this.beanFactory.getBean(containerFactoryBeanName, RabbitListenerContainerFactory.class); } @@ -535,13 +559,12 @@ private void resolveExecutor(MethodRabbitListenerEndpoint endpoint, RabbitListen Object execTarget, String beanName) { Object resolved = resolveExpression(rabbitListener.executor()); - if (resolved instanceof TaskExecutor) { - endpoint.setTaskExecutor((TaskExecutor) resolved); + if (resolved instanceof TaskExecutor tex) { + endpoint.setTaskExecutor(tex); } else { String execBeanName = resolveExpressionAsString(rabbitListener.executor(), "executor"); if (StringUtils.hasText(execBeanName)) { - assertBeanFactory(); try { endpoint.setTaskExecutor(this.beanFactory.getBean(execBeanName, TaskExecutor.class)); } @@ -557,13 +580,12 @@ private void resolvePostProcessor(MethodRabbitListenerEndpoint endpoint, RabbitL Object target, String beanName) { Object resolved = resolveExpression(rabbitListener.replyPostProcessor()); - if (resolved instanceof ReplyPostProcessor) { - endpoint.setReplyPostProcessor((ReplyPostProcessor) resolved); + if (resolved instanceof ReplyPostProcessor rpp) { + endpoint.setReplyPostProcessor(rpp); } else { String ppBeanName = resolveExpressionAsString(rabbitListener.replyPostProcessor(), "replyPostProcessor"); if (StringUtils.hasText(ppBeanName)) { - assertBeanFactory(); try { endpoint.setReplyPostProcessor(this.beanFactory.getBean(ppBeanName, ReplyPostProcessor.class)); } @@ -579,13 +601,12 @@ private void resolveMessageConverter(MethodRabbitListenerEndpoint endpoint, Rabb Object target, String beanName) { Object resolved = resolveExpression(rabbitListener.messageConverter()); - if (resolved instanceof MessageConverter) { - endpoint.setMessageConverter((MessageConverter) resolved); + if (resolved instanceof MessageConverter converter) { + endpoint.setMessageConverter(converter); } else { String mcBeanName = resolveExpressionAsString(rabbitListener.messageConverter(), "messageConverter"); if (StringUtils.hasText(mcBeanName)) { - assertBeanFactory(); try { endpoint.setMessageConverter(this.beanFactory.getBean(mcBeanName, MessageConverter.class)); } @@ -605,10 +626,6 @@ private void resolveReplyContentType(MethodRabbitListenerEndpoint endpoint, Rabb } } - protected void assertBeanFactory() { - Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name"); - } - protected String noBeanFoundMessage(Object target, String listenerBeanName, String requestedBeanName, Class expectedClass) { return "Could not register rabbit listener endpoint on [" @@ -625,23 +642,27 @@ private String getEndpointId(RabbitListener rabbitListener) { } } - private String[] resolveQueues(RabbitListener rabbitListener, Collection declarables) { + private List resolveQueues(RabbitListener rabbitListener, Collection declarables) { String[] queues = rabbitListener.queues(); QueueBinding[] bindings = rabbitListener.bindings(); org.springframework.amqp.rabbit.annotation.Queue[] queuesToDeclare = rabbitListener.queuesToDeclare(); - List result = new ArrayList(); - if (queues.length > 0) { - for (int i = 0; i < queues.length; i++) { - resolveAsString(resolveExpression(queues[i]), result, true, "queues"); - } + List queueNames = new ArrayList<>(); + List queueBeans = new ArrayList<>(); + for (String queue : queues) { + resolveQueues(queue, queueNames, queueBeans); + } + if (!queueNames.isEmpty()) { + // revert to the previous behavior of just using the name when there is mixture of String and Queue + queueBeans.forEach(qb -> queueNames.add(qb.getName())); + queueBeans.clear(); } if (queuesToDeclare.length > 0) { if (queues.length > 0) { throw new BeanInitializationException( "@RabbitListener can have only one of 'queues', 'queuesToDeclare', or 'bindings'"); } - for (int i = 0; i < queuesToDeclare.length; i++) { - result.add(declareQueue(queuesToDeclare[i], declarables)); + for (org.springframework.amqp.rabbit.annotation.Queue queue : queuesToDeclare) { + queueNames.add(declareQueue(queue, declarables)); } } if (bindings.length > 0) { @@ -649,26 +670,47 @@ private String[] resolveQueues(RabbitListener rabbitListener, Collection (Object) s) + .collect(Collectors.toList()); + } + return queueNames.isEmpty() + ? queueBeans.stream() + .map(s -> (Object) s) + .collect(Collectors.toList()) + : queueNames.stream() + .map(s -> (Object) s) + .collect(Collectors.toList()); + + } + + private void resolveQueues(String queue, List result, List queueBeans) { + resolveAsStringOrQueue(resolveExpression(queue), result, queueBeans, "queues"); } @SuppressWarnings("unchecked") - private void resolveAsString(Object resolvedValue, List result, boolean canBeQueue, String what) { + private void resolveAsStringOrQueue(@Nullable Object resolvedValue, List names, + @Nullable List queues, String what) { + Object resolvedValueToUse = resolvedValue; - if (resolvedValue instanceof String[]) { - resolvedValueToUse = Arrays.asList((String[]) resolvedValue); + if (resolvedValue instanceof String[] strings) { + resolvedValueToUse = Arrays.asList(strings); } - if (canBeQueue && resolvedValueToUse instanceof Queue) { - result.add(((Queue) resolvedValueToUse).getName()); + if (queues != null && resolvedValueToUse instanceof Queue q) { + if (!names.isEmpty()) { + // revert to the previous behavior of just using the name when there is mixture of String and Queue + names.add(q.getName()); + } + else { + queues.add(q); + } } - else if (resolvedValueToUse instanceof String) { - result.add((String) resolvedValueToUse); + else if (resolvedValueToUse instanceof String str) { + names.add(str); } else if (resolvedValueToUse instanceof Iterable) { for (Object object : (Iterable) resolvedValueToUse) { - resolveAsString(object, result, canBeQueue, what); + resolveAsStringOrQueue(object, names, queues, what); } } else { @@ -676,13 +718,13 @@ else if (resolvedValueToUse instanceof Iterable) { "@RabbitListener." + what + " can't resolve '%s' as a String[] or a String " - + (canBeQueue ? "or a Queue" : ""), + + (queues != null ? "or a Queue" : ""), resolvedValue)); } } private String[] registerBeansForDeclaration(RabbitListener rabbitListener, Collection declarables) { - List queues = new ArrayList(); + List queues = new ArrayList<>(); if (this.beanFactory instanceof ConfigurableBeanFactory) { for (QueueBinding binding : rabbitListener.bindings()) { String queueName = declareQueue(binding.value(), declarables); @@ -690,7 +732,7 @@ private String[] registerBeansForDeclaration(RabbitListener rabbitListener, Coll declareExchangeAndBinding(binding, queueName, declarables); } } - return queues.toArray(new String[queues.size()]); + return queues.toArray(new String[0]); } private String declareQueue(org.springframework.amqp.rabbit.annotation.Queue bindingQueue, @@ -717,6 +759,7 @@ private String declareQueue(org.springframework.amqp.rabbit.annotation.Queue bin return queueName; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void declareExchangeAndBinding(QueueBinding binding, String queueName, Collection declarables) { org.springframework.amqp.rabbit.annotation.Exchange bindingExchange = binding.exchange(); String exchangeName = resolveExpressionAsString(bindingExchange.value(), "@Exchange.exchange"); @@ -749,7 +792,7 @@ private void declareExchangeAndBinding(QueueBinding binding, String queueName, C exchangeBuilder.admins((Object[]) bindingExchange.admins()); } - Map arguments = resolveArguments(bindingExchange.arguments()); + Map arguments = resolveArguments(bindingExchange.arguments()); if (!CollectionUtils.isEmpty(arguments)) { exchangeBuilder.withArguments(arguments); @@ -768,6 +811,7 @@ private void declareExchangeAndBinding(QueueBinding binding, String queueName, C private void registerBindings(QueueBinding binding, String queueName, String exchangeName, String exchangeType, Collection declarables) { + final List routingKeys; if (exchangeType.equals(ExchangeTypes.FANOUT) || binding.key().length == 0) { routingKeys = Collections.singletonList(""); @@ -776,10 +820,10 @@ private void registerBindings(QueueBinding binding, String queueName, String exc final int length = binding.key().length; routingKeys = new ArrayList<>(); for (int i = 0; i < length; ++i) { - resolveAsString(resolveExpression(binding.key()[i]), routingKeys, false, "@QueueBinding.key"); + resolveAsStringOrQueue(resolveExpression(binding.key()[i]), routingKeys, null, "@QueueBinding.key"); } } - final Map bindingArguments = resolveArguments(binding.arguments()); + final Map bindingArguments = resolveArguments(binding.arguments()); final boolean bindingIgnoreExceptions = resolveExpressionAsBoolean(binding.ignoreDeclarationExceptions()); boolean declare = resolveExpressionAsBoolean(binding.declare()); for (String routingKey : routingKeys) { @@ -796,8 +840,8 @@ private void registerBindings(QueueBinding binding, String queueName, String exc } } - private Map resolveArguments(Argument[] arguments) { - Map map = new HashMap(); + private @Nullable Map resolveArguments(Argument[] arguments) { + Map map = new HashMap<>(); for (Argument arg : arguments) { String key = resolveExpressionAsString(arg.name(), "@Argument.name"); if (StringUtils.hasText(key)) { @@ -805,13 +849,13 @@ private Map resolveArguments(Argument[] arguments) { Object type = resolveExpression(arg.type()); Class typeClass; String typeName; - if (type instanceof Class) { - typeClass = (Class) type; + if (type instanceof Class clazz) { + typeClass = clazz; typeName = typeClass.getName(); } else { - Assert.isTrue(type instanceof String, () -> "Type must resolve to a Class or String, but resolved to [" - + type.getClass().getName() + "]"); + Assert.isTrue(type instanceof String, () -> "Type must resolve to a Class or String, " + + "but resolved to [" + type + "]"); typeName = (String) type; try { typeClass = ClassUtils.forName(typeName, this.beanClassLoader); @@ -820,18 +864,18 @@ private Map resolveArguments(Argument[] arguments) { throw new IllegalStateException("Could not load class", e); } } - addToMap(map, key, value, typeClass, typeName); + addToMap(map, key, value == null ? "" : value, typeClass, typeName); } else { - if (this.logger.isDebugEnabled()) { - this.logger.debug("@Argument ignored because the name resolved to an empty String"); - } + this.logger.debug("@Argument ignored because the name resolved to an empty String"); } } - return map.size() < 1 ? null : map; + return map.isEmpty() ? null : map; } - private void addToMap(Map map, String key, Object value, Class typeClass, String typeName) { + private void addToMap(Map map, String key, Object value, Class typeClass, + String typeName) { + if (value.getClass().getName().equals(typeName)) { if (typeClass.equals(String.class) && !StringUtils.hasText((String) value)) { putEmpty(map, key); @@ -841,7 +885,7 @@ private void addToMap(Map map, String key, Object value, Class map, String key, Object value, Class map, String key) { + private void putEmpty(Map map, String key) { if (this.emptyStringArguments.contains(key)) { map.put(key, ""); } @@ -871,12 +915,11 @@ private boolean resolveExpressionAsBoolean(String value) { private boolean resolveExpressionAsBoolean(String value, boolean defaultValue) { Object resolved = resolveExpression(value); - if (resolved instanceof Boolean) { - return (Boolean) resolved; + if (resolved instanceof Boolean bool) { + return bool; } - else if (resolved instanceof String) { - final String s = (String) resolved; - return StringUtils.hasText(s) ? Boolean.parseBoolean(s) : defaultValue; + else if (resolved instanceof String str) { + return StringUtils.hasText(str) ? Boolean.parseBoolean(str) : defaultValue; } else { return defaultValue; @@ -885,33 +928,33 @@ else if (resolved instanceof String) { protected String resolveExpressionAsString(String value, String attribute) { Object resolved = resolveExpression(value); - if (resolved instanceof String) { - return (String) resolved; + if (resolved instanceof String str) { + return str; } else { throw new IllegalStateException("The [" + attribute + "] must resolve to a String. " - + "Resolved to [" + resolved.getClass() + "] for [" + value + "]"); + + "Resolved to [" + resolved + "] for [" + value + "]"); } } - private String resolveExpressionAsStringOrInteger(String value, String attribute) { + private @Nullable String resolveExpressionAsStringOrInteger(String value, String attribute) { if (!StringUtils.hasLength(value)) { return null; } Object resolved = resolveExpression(value); - if (resolved instanceof String) { - return (String) resolved; + if (resolved instanceof String str) { + return str; } else if (resolved instanceof Integer) { return resolved.toString(); } else { throw new IllegalStateException("The [" + attribute + "] must resolve to a String. " - + "Resolved to [" + resolved.getClass() + "] for [" + value + "]"); + + "Resolved to [" + resolved + "] for [" + value + "]"); } } - private Object resolveExpression(String value) { + protected @Nullable Object resolveExpression(String value) { String resolvedValue = resolve(value); return this.resolver.evaluate(resolvedValue, this.expressionContext); @@ -923,9 +966,9 @@ private Object resolveExpression(String value) { * @return the resolved value. * @see ConfigurableBeanFactory#resolveEmbeddedValue */ - private String resolve(String value) { - if (this.beanFactory != null && this.beanFactory instanceof ConfigurableBeanFactory) { - return ((ConfigurableBeanFactory) this.beanFactory).resolveEmbeddedValue(value); + private @Nullable String resolve(String value) { + if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { + return cbf.resolveEmbeddedValue(value); } return value; } @@ -938,7 +981,10 @@ private String resolve(String value) { */ private class RabbitHandlerMethodFactoryAdapter implements MessageHandlerMethodFactory { - private MessageHandlerMethodFactory factory; + private final DefaultFormattingConversionService defaultFormattingConversionService = + new DefaultFormattingConversionService(); + + private @Nullable MessageHandlerMethodFactory factory; RabbitHandlerMethodFactoryAdapter() { } @@ -960,20 +1006,21 @@ private MessageHandlerMethodFactory getFactory() { } private MessageHandlerMethodFactory createDefaultMessageHandlerMethodFactory() { - DefaultMessageHandlerMethodFactory defaultFactory = new DefaultMessageHandlerMethodFactory(); + DefaultMessageHandlerMethodFactory defaultFactory = new AmqpMessageHandlerMethodFactory(); Validator validator = RabbitListenerAnnotationBeanPostProcessor.this.registrar.getValidator(); if (validator != null) { defaultFactory.setValidator(validator); } defaultFactory.setBeanFactory(RabbitListenerAnnotationBeanPostProcessor.this.beanFactory); - DefaultConversionService conversionService = new DefaultConversionService(); - conversionService.addConverter( + this.defaultFormattingConversionService.addConverter( new BytesToStringConverter(RabbitListenerAnnotationBeanPostProcessor.this.charset)); - defaultFactory.setConversionService(conversionService); + defaultFactory.setConversionService(this.defaultFormattingConversionService); - List customArgumentsResolver = - new ArrayList<>(RabbitListenerAnnotationBeanPostProcessor.this.registrar.getCustomMethodArgumentResolvers()); + List customArgumentsResolver = new ArrayList<>( + RabbitListenerAnnotationBeanPostProcessor.this.registrar.getCustomMethodArgumentResolvers()); defaultFactory.setCustomArgumentResolvers(customArgumentsResolver); + defaultFactory.setMessageConverter(new GenericMessageConverter(this.defaultFormattingConversionService)); + defaultFactory.afterPropertiesSet(); return defaultFactory; } @@ -1010,37 +1057,23 @@ private TypeMetadata() { } TypeMetadata(ListenerMethod[] methods, Method[] multiMethods, RabbitListener[] classLevelListeners) { // NOSONAR - this.listenerMethods = methods; - this.handlerMethods = multiMethods; - this.classAnnotations = classLevelListeners; + this.listenerMethods = methods; // NOSONAR + this.handlerMethods = multiMethods; // NOSONAR + this.classAnnotations = classLevelListeners; // NOSONAR } } /** * A method annotated with {@link RabbitListener}, together with the annotations. + * + * @param method the method with annotations + * @param annotations on the method */ - private static class ListenerMethod { - - final Method method; // NOSONAR - - final RabbitListener[] annotations; // NOSONAR - - ListenerMethod(Method method, RabbitListener[] annotations) { // NOSONAR - this.method = method; - this.annotations = annotations; - } - + private record ListenerMethod(Method method, RabbitListener[] annotations) { } - private static class BytesToStringConverter implements Converter { - - - private final Charset charset; - - BytesToStringConverter(Charset charset) { - this.charset = charset; - } + private record BytesToStringConverter(Charset charset) implements Converter { @Override public String convert(byte[] source) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/package-info.java index 3cd91d6180..b46a1207f5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/package-info.java @@ -2,4 +2,5 @@ * Annotations and supporting classes for declarative Rabbit listener * endpoint */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.annotation; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java new file mode 100644 index 0000000000..11f90e8124 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.aot; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.rabbit.connection.ChannelProxy; +import org.springframework.amqp.rabbit.connection.PublisherCallbackChannel; +import org.springframework.aop.SpringProxy; +import org.springframework.aop.framework.Advised; +import org.springframework.aot.hint.ProxyHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.core.DecoratingProxy; + +/** + * {@link RuntimeHintsRegistrar} for spring-rabbit. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class RabbitRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + ProxyHints proxyHints = hints.proxies(); + proxyHints.registerJdkProxy(ChannelProxy.class); + proxyHints.registerJdkProxy(ChannelProxy.class, PublisherCallbackChannel.class); + proxyHints.registerJdkProxy(builder -> + builder.proxiedInterfaces(TypeReference.of( + "org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer$ContainerDelegate")) + .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java new file mode 100644 index 0000000000..31fe4aae08 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides classes to support Spring AOT. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.rabbit.aot; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/BatchingStrategy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/BatchingStrategy.java index 1fec17fab8..3565d5b8d2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/BatchingStrategy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/BatchingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author 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.Date; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; @@ -41,11 +43,13 @@ public interface BatchingStrategy { * @param message The message. * @return The batched message ({@link MessageBatch}), or null if not ready to release. */ - MessageBatch addToBatch(String exchange, String routingKey, Message message); + @Nullable + MessageBatch addToBatch(@Nullable String exchange, @Nullable String routingKey, Message message); /** * @return the date the next scheduled release should run, or null if no data to release. */ + @Nullable Date nextRelease(); /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/MessageBatch.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/MessageBatch.java index 05c17c28e8..ac3759cbcf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/MessageBatch.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/MessageBatch.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,47 +16,49 @@ package org.springframework.amqp.rabbit.batch; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; /** * An object encapsulating a {@link Message} containing the batch of messages, * the exchange, and routing key. * + * @param exchange the exchange for batch of messages + * @param routingKey the routing key for batch + * @param message the message with a batch + * * @author Gary Russell + * @author Artem Bilan + * * @since 1.4.1 * */ -public class MessageBatch { - - private final String exchange; - - private final String routingKey; - - private final Message message; - - public MessageBatch(String exchange, String routingKey, Message message) { - this.exchange = exchange; - this.routingKey = routingKey; - this.message = message; - } +public record MessageBatch(@Nullable String exchange, @Nullable String routingKey, Message message) { /** * @return the exchange + * @deprecated in favor or {@link #exchange()}. */ - public String getExchange() { + @Deprecated(forRemoval = true, since = "4.0") + public @Nullable String getExchange() { return this.exchange; } /** * @return the routingKey + * @deprecated in favor or {@link #routingKey()}. */ - public String getRoutingKey() { + @Deprecated(forRemoval = true, since = "4.0") + public @Nullable String getRoutingKey() { return this.routingKey; } /** * @return the message + * @deprecated in favor or {@link #message()} ()}. */ + @Deprecated(forRemoval = true, since = "4.0") public Message getMessage() { return this.message; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java index 504fe548a7..3f1bbb0342 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author 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,14 @@ import java.util.List; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.beans.BeanUtils; import org.springframework.util.Assert; /** @@ -38,6 +41,7 @@ * length field. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.4.1 * */ @@ -49,11 +53,11 @@ public class SimpleBatchingStrategy implements BatchingStrategy { private final long timeout; - private final List messages = new ArrayList(); + private final List messages = new ArrayList<>(); - private String exchange; + private @Nullable String exchange; - private String routingKey; + private @Nullable String routingKey; private int currentSize; @@ -70,7 +74,7 @@ public SimpleBatchingStrategy(int batchSize, int bufferLimit, long timeout) { } @Override - public MessageBatch addToBatch(String exch, String routKey, Message message) { + public @Nullable MessageBatch addToBatch(@Nullable String exch, @Nullable String routKey, Message message) { if (this.exchange != null) { Assert.isTrue(this.exchange.equals(exch), "Cannot send to different exchanges in the same batch"); } @@ -86,7 +90,7 @@ public MessageBatch addToBatch(String exch, String routKey, Message message) { } int bufferUse = Integer.BYTES + message.getBody().length; MessageBatch batch = null; - if (this.messages.size() > 0 && this.currentSize + bufferUse > this.bufferLimit) { + if (!this.messages.isEmpty() && this.currentSize + bufferUse > this.bufferLimit) { batch = doReleaseBatch(); this.exchange = exch; this.routingKey = routKey; @@ -101,17 +105,16 @@ public MessageBatch addToBatch(String exch, String routKey, Message message) { } @Override - public Date nextRelease() { - if (this.messages.size() == 0 || this.timeout <= 0) { + public @Nullable Date nextRelease() { + if (this.messages.isEmpty() || this.timeout <= 0) { return null; } - else if (this.currentSize >= this.bufferLimit) { + if (this.currentSize >= this.bufferLimit) { // release immediately, we're already over the limit return new Date(); } - else { - return new Date(System.currentTimeMillis() + this.timeout); - } + + return new Date(System.currentTimeMillis() + this.timeout); } @Override @@ -120,13 +123,12 @@ public Collection releaseBatches() { if (batch == null) { return Collections.emptyList(); } - else { - return Collections.singletonList(batch); - } + + return Collections.singletonList(batch); } - private MessageBatch doReleaseBatch() { - if (this.messages.size() < 1) { + private @Nullable MessageBatch doReleaseBatch() { + if (this.messages.isEmpty()) { return null; } Message message = assembleMessage(); @@ -184,10 +186,16 @@ public void deBatch(Message message, Consumer fragmentConsumer) { byte[] body = new byte[length]; byteBuffer.get(body); messageProperties.setContentLength(length); - // Caveat - shared MessageProperties. - Message fragment = new Message(body, messageProperties); - if (!byteBuffer.hasRemaining()) { - messageProperties.setLastInBatch(true); + // Caveat - shared MessageProperties, except for last + Message fragment; + if (byteBuffer.hasRemaining()) { + fragment = new Message(body, messageProperties); + } + else { + MessageProperties lastProperties = new MessageProperties(); + BeanUtils.copyProperties(messageProperties, lastProperties); + lastProperties.setLastInBatch(true); + fragment = new Message(body, lastProperties); } fragmentConsumer.accept(fragment); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/package-info.java index 9901fe182b..eacc718a85 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes for message batching. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.batch; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java index 705fd84056..4a83e75e99 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; @@ -34,11 +35,12 @@ * @author Gary Russell * @author Felipe Gutierrez * @author Artem Bilan + * @author Ngoc Nhan * */ public abstract class AbstractExchangeParser extends AbstractSingleBeanDefinitionParser { - private static final ThreadLocal CURRENT_ELEMENT = new ThreadLocal<>(); + private static final ThreadLocal<@Nullable Element> CURRENT_ELEMENT = new ThreadLocal<>(); private static final String ARGUMENTS_ELEMENT = "exchange-arguments"; @@ -88,7 +90,7 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit NamespaceUtils.setValueIfAttributeDefined(builder, element, DELAYED_ATTRIBUTE); NamespaceUtils.setValueIfAttributeDefined(builder, element, "internal"); - this.parseArguments(element, ARGUMENTS_ELEMENT, parserContext, builder, null); + parseArguments(element, ARGUMENTS_ELEMENT, parserContext, builder, null); NamespaceUtils.parseDeclarationControls(element, builder); CURRENT_ELEMENT.set(element); @@ -96,12 +98,14 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit protected void parseBindings(Element element, ParserContext parserContext, BeanDefinitionBuilder builder, String exchangeName) { + Element bindingsElement = DomUtils.getChildElementByTagName(element, BINDINGS_ELE); doParseBindings(element, parserContext, exchangeName, bindingsElement, this); } protected void doParseBindings(Element element, ParserContext parserContext, - String exchangeName, Element bindings, AbstractExchangeParser parser) { + String exchangeName, @Nullable Element bindings, AbstractExchangeParser parser) { + if (bindings != null) { for (Element binding : DomUtils.getChildElementsByTagName(bindings, BINDING_ELE)) { BeanDefinitionBuilder bindingBuilder = parser.parseBinding(exchangeName, binding, @@ -136,7 +140,7 @@ protected void parseDestination(Element binding, ParserContext parserContext, Be } private void parseArguments(Element element, String argumentsElementName, ParserContext parserContext, - BeanDefinitionBuilder builder, String propertyName) { + BeanDefinitionBuilder builder, @Nullable String propertyName) { Element argumentsElement = DomUtils.getChildElementByTagName(element, argumentsElementName); if (argumentsElement != null) { @@ -144,7 +148,7 @@ private void parseArguments(Element element, String argumentsElementName, Parser Map map = parserContext.getDelegate().parseMapElement(argumentsElement, builder.getRawBeanDefinition()); if (StringUtils.hasText(ref)) { - if (map != null && map.size() > 0) { + if (!map.isEmpty()) { parserContext.getReaderContext().error("You cannot have both a 'ref' and a nested map", element); } if (propertyName == null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index d94ad2a2dc..f51a3d5754 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.rabbit.config; - import java.util.Arrays; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; @@ -24,22 +23,22 @@ import org.aopalliance.aop.Advice; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.MessageAckListener; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; import org.springframework.amqp.support.ConsumerTagStrategy; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.utils.JavaUtils; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; @@ -68,55 +67,57 @@ public abstract class AbstractRabbitListenerContainerFactory containerCustomizer; - private Integer phase; + private boolean batchListener; - private MessagePostProcessor[] afterReceivePostProcessors; + private @Nullable BatchingStrategy batchingStrategy; - private ContainerCustomizer containerCustomizer; + private @Nullable Boolean deBatchingEnabled; - private boolean batchListener; + private @Nullable MessageAckListener messageAckListener; - private BatchingStrategy batchingStrategy; + private @Nullable RabbitListenerObservationConvention observationConvention; - private Boolean deBatchingEnabled; + private @Nullable Boolean forceStop; /** * @param connectionFactory The connection factory. @@ -182,23 +183,6 @@ public void setPrefetchCount(Integer prefetch) { this.prefetchCount = prefetch; } - /** - * @return the advice chain that was set. Defaults to {@code null}. - * @since 1.7.4 - */ - @Nullable - public Advice[] getAdviceChain() { - return this.adviceChain == null ? null : Arrays.copyOf(this.adviceChain, this.adviceChain.length); - } - - /** - * @param adviceChain the advice chain to set. - * @see AbstractMessageListenerContainer#setAdviceChain - */ - public void setAdviceChain(Advice... adviceChain) { - this.adviceChain = adviceChain == null ? null : Arrays.copyOf(adviceChain, adviceChain.length); - } - /** * @param recoveryInterval The recovery interval. * @see AbstractMessageListenerContainer#setRecoveryInterval @@ -258,11 +242,6 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv this.applicationEventPublisher = applicationEventPublisher; } - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - /** * @param autoStartup true for auto startup. * @see AbstractMessageListenerContainer#setAutoStartup(boolean) @@ -323,9 +302,9 @@ public void setBatchingStrategy(BatchingStrategy batchingStrategy) { } /** - * Determine whether or not the container should de-batch batched + * Determine whether the container should de-batch batched * messages (true) or call the listener with the batch (false). Default: true. - * @param deBatchingEnabled whether or not to disable de-batching of messages. + * @param deBatchingEnabled whether to disable de-batching of messages. * @since 2.2 * @see AbstractMessageListenerContainer#setDeBatchingEnabled(boolean) */ @@ -343,8 +322,37 @@ public void setGlobalQos(boolean globalQos) { this.globalQos = globalQos; } + /** + * Set a {@link MessageAckListener} to use when ack a message(messages) in + * {@link AcknowledgeMode#AUTO} mode. + * @param messageAckListener the messageAckListener. + * @since 2.4.6 + */ + public void setMessageAckListener(MessageAckListener messageAckListener) { + this.messageAckListener = messageAckListener; + } + + /** + * Set an observation convention; used to add additional key/values to observations. + * @param observationConvention the convention. + * @since 3.0 + */ + public void setObservationConvention(RabbitListenerObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + + /** + * Set to true to stop the container after the current message(s) are processed and + * requeue any prefetched. Useful when using exclusive or single-active consumers. + * @param forceStop true to stop when current message(s) are processed. + * @since 2.4.15 + */ + public void setForceStop(boolean forceStop) { + this.forceStop = forceStop; + } + @Override - public C createListenerContainer(RabbitListenerEndpoint endpoint) { + public C createListenerContainer(@Nullable RabbitListenerEndpoint endpoint) { C instance = createContainerInstance(); JavaUtils javaUtils = @@ -354,38 +362,47 @@ public C createListenerContainer(RabbitListenerEndpoint endpoint) { if (this.messageConverter != null && endpoint != null && endpoint.getMessageConverter() == null) { endpoint.setMessageConverter(this.messageConverter); } + Advice[] adviceChain = getAdviceChain(); javaUtils - .acceptIfNotNull(this.acknowledgeMode, instance::setAcknowledgeMode) - .acceptIfNotNull(this.channelTransacted, instance::setChannelTransacted) - .acceptIfNotNull(this.applicationContext, instance::setApplicationContext) - .acceptIfNotNull(this.taskExecutor, instance::setTaskExecutor) - .acceptIfNotNull(this.transactionManager, instance::setTransactionManager) - .acceptIfNotNull(this.prefetchCount, instance::setPrefetchCount) - .acceptIfNotNull(this.globalQos, instance::setGlobalQos) - .acceptIfNotNull(getDefaultRequeueRejected(), instance::setDefaultRequeueRejected) - .acceptIfNotNull(this.adviceChain, instance::setAdviceChain) - .acceptIfNotNull(this.recoveryBackOff, instance::setRecoveryBackOff) - .acceptIfNotNull(this.mismatchedQueuesFatal, instance::setMismatchedQueuesFatal) - .acceptIfNotNull(this.missingQueuesFatal, instance::setMissingQueuesFatal) - .acceptIfNotNull(this.consumerTagStrategy, instance::setConsumerTagStrategy) - .acceptIfNotNull(this.idleEventInterval, instance::setIdleEventInterval) - .acceptIfNotNull(this.failedDeclarationRetryInterval, instance::setFailedDeclarationRetryInterval) - .acceptIfNotNull(this.applicationEventPublisher, instance::setApplicationEventPublisher) - .acceptIfNotNull(this.autoStartup, instance::setAutoStartup) - .acceptIfNotNull(this.phase, instance::setPhase) - .acceptIfNotNull(this.afterReceivePostProcessors, instance::setAfterReceivePostProcessors) - .acceptIfNotNull(this.deBatchingEnabled, instance::setDeBatchingEnabled); + .acceptIfNotNull(this.acknowledgeMode, instance::setAcknowledgeMode) + .acceptIfNotNull(this.channelTransacted, instance::setChannelTransacted) + .acceptIfNotNull(getApplicationContext(), instance::setApplicationContext) + .acceptIfNotNull(this.taskExecutor, instance::setTaskExecutor) + .acceptIfNotNull(this.transactionManager, instance::setTransactionManager) + .acceptIfNotNull(this.prefetchCount, instance::setPrefetchCount) + .acceptIfNotNull(this.globalQos, instance::setGlobalQos) + .acceptIfNotNull(getDefaultRequeueRejected(), instance::setDefaultRequeueRejected) + .acceptIfNotNull(adviceChain, instance::setAdviceChain) + .acceptIfNotNull(this.recoveryBackOff, instance::setRecoveryBackOff) + .acceptIfNotNull(this.mismatchedQueuesFatal, instance::setMismatchedQueuesFatal) + .acceptIfNotNull(this.missingQueuesFatal, instance::setMissingQueuesFatal) + .acceptIfNotNull(this.consumerTagStrategy, instance::setConsumerTagStrategy) + .acceptIfNotNull(this.idleEventInterval, instance::setIdleEventInterval) + .acceptIfNotNull(this.failedDeclarationRetryInterval, instance::setFailedDeclarationRetryInterval) + .acceptIfNotNull(this.applicationEventPublisher, instance::setApplicationEventPublisher) + .acceptIfNotNull(this.autoStartup, instance::setAutoStartup) + .acceptIfNotNull(this.phase, instance::setPhase) + .acceptIfNotNull(this.afterReceivePostProcessors, instance::setAfterReceivePostProcessors) + .acceptIfNotNull(this.deBatchingEnabled, instance::setDeBatchingEnabled) + .acceptIfNotNull(this.messageAckListener, instance::setMessageAckListener) + .acceptIfNotNull(this.batchingStrategy, instance::setBatchingStrategy) + .acceptIfNotNull(getMicrometerEnabled(), instance::setMicrometerEnabled) + .acceptIfNotNull(getObservationEnabled(), instance::setObservationEnabled) + .acceptIfNotNull(this.observationConvention, instance::setObservationConvention) + .acceptIfNotNull(this.forceStop, instance::setForceStop); if (this.batchListener && this.deBatchingEnabled == null) { // turn off container debatching by default for batch listeners instance.setDeBatchingEnabled(false); } if (endpoint != null) { // endpoint settings overriding default factory settings javaUtils - .acceptIfNotNull(endpoint.getTaskExecutor(), instance::setTaskExecutor) - .acceptIfNotNull(endpoint.getAckMode(), instance::setAcknowledgeMode) - .acceptIfNotNull(this.batchingStrategy, endpoint::setBatchingStrategy); - instance.setListenerId(endpoint.getId()); - endpoint.setBatchListener(this.batchListener); + .acceptIfNotNull(endpoint.getTaskExecutor(), instance::setTaskExecutor) + .acceptIfNotNull(endpoint.getAckMode(), instance::setAcknowledgeMode) + .acceptIfNotNull(endpoint.getBatchingStrategy(), instance::setBatchingStrategy) + .acceptIfNotNull(endpoint.getId(), instance::setListenerId); + if (endpoint.getBatchListener() == null) { + endpoint.setBatchListener(this.batchListener); + } } applyCommonOverrides(endpoint, instance); @@ -411,7 +428,7 @@ public C createListenerContainer(RabbitListenerEndpoint endpoint) { * @param instance the container instance to configure. * @param endpoint the endpoint. */ - protected void initializeContainer(C instance, RabbitListenerEndpoint endpoint) { + protected void initializeContainer(C instance, @Nullable RabbitListenerEndpoint endpoint) { } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRetryOperationsInterceptorFactoryBean.java index c59fd91277..954c98cf0d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRetryOperationsInterceptorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.config; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.retry.MessageRecoverer; import org.springframework.beans.factory.FactoryBean; @@ -30,9 +31,9 @@ */ public abstract class AbstractRetryOperationsInterceptorFactoryBean implements FactoryBean { - private MessageRecoverer messageRecoverer; + private @Nullable MessageRecoverer messageRecoverer; - private RetryOperations retryTemplate; + private @Nullable RetryOperations retryTemplate; public void setRetryOperations(RetryOperations retryTemplate) { this.retryTemplate = retryTemplate; @@ -42,11 +43,11 @@ public void setMessageRecoverer(MessageRecoverer messageRecoverer) { this.messageRecoverer = messageRecoverer; } - protected RetryOperations getRetryOperations() { + protected @Nullable RetryOperations getRetryOperations() { return this.retryTemplate; } - protected MessageRecoverer getMessageRecoverer() { + protected @Nullable MessageRecoverer getMessageRecoverer() { return this.messageRecoverer; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AnnotationDrivenParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AnnotationDrivenParser.java index cd0541fc5e..a2736c1d39 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AnnotationDrivenParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AnnotationDrivenParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor; @@ -34,12 +35,14 @@ * Parser for the 'annotation-driven' element of the 'rabbit' namespace. * * @author Stephane Nicoll + * @author Artem Bilan + * * @since 1.4 */ class AnnotationDrivenParser implements BeanDefinitionParser { @Override - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { Object source = parserContext.extractSource(element); // Register component for the surrounding element. @@ -85,7 +88,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { return null; } - private static void registerDefaultEndpointRegistry(Object source, ParserContext parserContext) { + private static void registerDefaultEndpointRegistry(@Nullable Object source, ParserContext parserContext) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RabbitListenerEndpointRegistry.class); builder.getRawBeanDefinition().setSource(source); registerInfrastructureBean(parserContext, builder, diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java index 1dd5804d80..0e7178a169 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,22 @@ package org.springframework.amqp.rabbit.config; import java.util.Arrays; +import java.util.function.Function; + +import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener; +import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor; import org.springframework.amqp.utils.JavaUtils; -import org.springframework.lang.Nullable; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -35,22 +43,38 @@ * @param the container type that the factory creates. * * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.4 * */ public abstract class BaseRabbitListenerContainerFactory - implements RabbitListenerContainerFactory { + implements RabbitListenerContainerFactory, ApplicationContextAware { + + private @Nullable Boolean defaultRequeueRejected; + + private MessagePostProcessor @Nullable [] beforeSendReplyPostProcessors; + + private @Nullable RetryTemplate retryTemplate; + + private @Nullable RecoveryCallback recoveryCallback; - private Boolean defaultRequeueRejected; + private Advice @Nullable [] adviceChain; - private MessagePostProcessor[] beforeSendReplyPostProcessors; + private @Nullable Function<@Nullable String, @Nullable ReplyPostProcessor> replyPostProcessorProvider; - private RetryTemplate retryTemplate; + private @Nullable Boolean micrometerEnabled; - private RecoveryCallback recoveryCallback; + private @Nullable Boolean observationEnabled; + + @SuppressWarnings("NullAway.Init") + private ApplicationContext applicationContext; + + private @Nullable String beanName; @Override - public abstract C createListenerContainer(RabbitListenerEndpoint endpoint); + public abstract C createListenerContainer(@Nullable RabbitListenerEndpoint endpoint); /** * @param requeueRejected true to reject by default. @@ -64,7 +88,7 @@ public void setDefaultRequeueRejected(Boolean requeueRejected) { * Return the defaultRequeueRejected. * @return the defaultRequeueRejected. */ - protected Boolean getDefaultRequeueRejected() { + protected @Nullable Boolean getDefaultRequeueRejected() { return this.defaultRequeueRejected; } @@ -103,27 +127,113 @@ public void setReplyRecoveryCallback(RecoveryCallback recoveryCallback) { this.recoveryCallback = recoveryCallback; } + /** + * Set a function to provide a reply post processor; it will be used if there is no + * replyPostProcessor on the rabbit listener annotation. The input parameter is the + * listener id. + * @param replyPostProcessorProvider the post processor. + * @since 3.0 + */ + public void setReplyPostProcessorProvider( + Function<@Nullable String, @Nullable ReplyPostProcessor> replyPostProcessorProvider) { + + this.replyPostProcessorProvider = replyPostProcessorProvider; + } + + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected void applyCommonOverrides(@Nullable RabbitListenerEndpoint endpoint, C instance) { if (endpoint != null) { // endpoint settings overriding default factory settings JavaUtils.INSTANCE - .acceptIfNotNull(endpoint.getAutoStartup(), instance::setAutoStartup); - instance.setListenerId(endpoint.getId()); + .acceptIfNotNull(endpoint.getAutoStartup(), instance::setAutoStartup) + .acceptIfNotNull(endpoint.getId(), instance::setListenerId); endpoint.setupListenerContainer(instance); } - if (instance.getMessageListener() instanceof AbstractAdaptableMessageListener) { - AbstractAdaptableMessageListener messageListener = (AbstractAdaptableMessageListener) instance - .getMessageListener(); + Object iml = instance.getMessageListener(); + if (iml instanceof AbstractAdaptableMessageListener messageListener) { JavaUtils.INSTANCE // NOSONAR .acceptIfNotNull(this.beforeSendReplyPostProcessors, messageListener::setBeforeSendReplyPostProcessors) .acceptIfNotNull(this.retryTemplate, messageListener::setRetryTemplate) .acceptIfCondition(this.retryTemplate != null && this.recoveryCallback != null, this.recoveryCallback, messageListener::setRecoveryCallback) - .acceptIfNotNull(this.defaultRequeueRejected, messageListener::setDefaultRequeueRejected) - .acceptIfNotNull(endpoint.getReplyPostProcessor(), messageListener::setReplyPostProcessor) - .acceptIfNotNull(endpoint.getReplyContentType(), messageListener::setReplyContentType); - messageListener.setConverterWinsContentType(endpoint.isConverterWinsContentType()); + .acceptIfNotNull(this.defaultRequeueRejected, messageListener::setDefaultRequeueRejected); + if (endpoint != null) { + JavaUtils.INSTANCE + .acceptIfNotNull(endpoint.getReplyPostProcessor(), messageListener::setReplyPostProcessor) + .acceptIfNotNull(endpoint.getReplyContentType(), messageListener::setReplyContentType); + messageListener.setConverterWinsContentType(endpoint.isConverterWinsContentType()); + if (endpoint.getReplyPostProcessor() == null && this.replyPostProcessorProvider != null) { + JavaUtils.INSTANCE + .acceptIfNotNull(this.replyPostProcessorProvider.apply(endpoint.getId()), + messageListener::setReplyPostProcessor); + } + } } } + /** + * @return the advice chain that was set. Defaults to {@code null}. + * @since 1.7.4 + */ + public Advice @Nullable [] getAdviceChain() { + return this.adviceChain == null ? null : Arrays.copyOf(this.adviceChain, this.adviceChain.length); + } + + /** + * @param adviceChain the advice chain to set. + * @see AbstractMessageListenerContainer#setAdviceChain + */ + public void setAdviceChain(Advice @Nullable ... adviceChain) { + this.adviceChain = adviceChain == null ? null : Arrays.copyOf(adviceChain, adviceChain.length); + } + + /** + * Set to {@code false} to disable micrometer listener timers. When true, ignored + * if {@link #setObservationEnabled(boolean)} is set to true. + * @param micrometerEnabled false to disable. + * @since 3.0 + * @see #setObservationEnabled(boolean) + */ + public void setMicrometerEnabled(boolean micrometerEnabled) { + this.micrometerEnabled = micrometerEnabled; + } + + protected @Nullable Boolean getMicrometerEnabled() { + return this.micrometerEnabled; + } + + /** + * Enable observation via micrometer; disables basic Micrometer timers enabled + * by {@link #setMicrometerEnabled(boolean)}. + * @param observationEnabled true to enable. + * @since 3.0 + * @see #setMicrometerEnabled(boolean) + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + protected @Nullable Boolean getObservationEnabled() { + return this.observationEnabled; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + protected ApplicationContext getApplicationContext() { + return this.applicationContext; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public @Nullable String getBeanName() { + return this.beanName; + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java index af8ca15a29..a15bce3165 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,37 +18,41 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Binding.DestinationType; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.Queue; import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.Assert; /** * @author Dave Syer * @author Gary Russell + * @author Artem Bilan * */ public class BindingFactoryBean implements FactoryBean { - private Map arguments; + private @Nullable Map arguments; private String routingKey = ""; - private String exchange; + private @Nullable String exchange; - private Queue destinationQueue; + private @Nullable Queue destinationQueue; - private Exchange destinationExchange; + private @Nullable Exchange destinationExchange; - private Boolean shouldDeclare; + private @Nullable Boolean shouldDeclare; - private Boolean ignoreDeclarationExceptions; + private @Nullable Boolean ignoreDeclarationExceptions; - private AmqpAdmin[] adminsThatShouldDeclare; + private AmqpAdmin @Nullable [] adminsThatShouldDeclare; - public void setArguments(Map arguments) { + public void setArguments(Map arguments) { this.arguments = arguments; } @@ -77,7 +81,7 @@ public void setIgnoreDeclarationExceptions(Boolean ignoreDeclarationExceptions) } public void setAdminsThatShouldDeclare(AmqpAdmin... admins) { // NOSONAR - this.adminsThatShouldDeclare = admins; + this.adminsThatShouldDeclare = admins; // NOSONAR } @Override @@ -89,6 +93,7 @@ public Binding getObject() { destinationType = DestinationType.QUEUE; } else { + Assert.notNull(this.destinationExchange, "Or 'destinationExchange', or 'destinationQueue' must be provided"); destination = this.destinationExchange.getName(); destinationType = DestinationType.EXCHANGE; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java new file mode 100644 index 0000000000..1ec652c818 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java @@ -0,0 +1,54 @@ +/* + * Copyright 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.amqp.rabbit.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.util.Assert; + +/** + * Implementation of {@link ContainerCustomizer} providing the configuration of + * multiple customizers at the same time. + * + * @param the container type. + * + * @author Rene Felgentraeger + * @author Gary Russell + * + * @since 2.4.8 + */ +public class CompositeContainerCustomizer implements ContainerCustomizer { + + private final List> customizers = new ArrayList<>(); + + /** + * Create an instance with the provided delegate customizers. + * @param customizers the customizers. + */ + public CompositeContainerCustomizer(List> customizers) { + Assert.notNull(customizers, "At least one customizer must be present"); + this.customizers.addAll(customizers); + } + + @Override + public void configure(C container) { + this.customizers.forEach(c -> c.configure(container)); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/DirectRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/DirectRabbitListenerContainerFactory.java index 33b25be6f9..8d8413cb5d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/DirectRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/DirectRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.utils.JavaUtils; @@ -32,15 +34,15 @@ public class DirectRabbitListenerContainerFactory extends AbstractRabbitListenerContainerFactory { - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; - private Long monitorInterval; + private @Nullable Long monitorInterval; private Integer consumersPerQueue = 1; - private Integer messagesPerAck; + private @Nullable Integer messagesPerAck; - private Long ackTimeout; + private @Nullable Long ackTimeout; /** * Set the task scheduler to use for the task that monitors idle containers and @@ -53,7 +55,7 @@ public void setTaskScheduler(TaskScheduler taskScheduler) { /** * Set how often to run a task to check for failed consumers and idle containers. - * @param monitorInterval the interval; default 10000 but it will be adjusted down + * @param monitorInterval the interval; default 10000, but it will be adjusted down * to the smallest of this, {@link #setIdleEventInterval(Long) idleEventInterval} / 2 * (if configured) or * {@link #setFailedDeclarationRetryInterval(Long) failedDeclarationRetryInterval}. @@ -102,13 +104,15 @@ protected DirectMessageListenerContainer createContainerInstance() { } @Override - protected void initializeContainer(DirectMessageListenerContainer instance, RabbitListenerEndpoint endpoint) { + protected void initializeContainer(DirectMessageListenerContainer instance, + @Nullable RabbitListenerEndpoint endpoint) { + super.initializeContainer(instance, endpoint); JavaUtils javaUtils = JavaUtils.INSTANCE.acceptIfNotNull(this.taskScheduler, instance::setTaskScheduler) - .acceptIfNotNull(this.monitorInterval, instance::setMonitorInterval) - .acceptIfNotNull(this.messagesPerAck, instance::setMessagesPerAck) - .acceptIfNotNull(this.ackTimeout, instance::setAckTimeout); + .acceptIfNotNull(this.monitorInterval, instance::setMonitorInterval) + .acceptIfNotNull(this.messagesPerAck, instance::setMessagesPerAck) + .acceptIfNotNull(this.ackTimeout, instance::setAckTimeout); if (endpoint != null && endpoint.getConcurrency() != null) { try { instance.setConsumersPerQueue(Integer.parseInt(endpoint.getConcurrency())); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/HeadersExchangeParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/HeadersExchangeParser.java index fc38d35a90..0cb49dbab2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/HeadersExchangeParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/HeadersExchangeParser.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,6 +30,7 @@ * @author Dave Syer * @author Gary Russell * @author Artem Bilan + * @author Ngoc Nhan * */ public class HeadersExchangeParser extends AbstractExchangeParser { @@ -63,7 +64,7 @@ protected BeanDefinitionBuilder parseBinding(String exchangeName, Element bindin parserContext.getReaderContext() .error("At least one of 'binding-arguments' sub-element or 'key/value' attributes pair have to be declared.", binding); } - ManagedMap map = new ManagedMap(); + ManagedMap map = new ManagedMap<>(); map.put(new TypedStringValue(key), new TypedStringValue(value)); builder.addPropertyValue("arguments", map); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java index 07c7be9f41..3f83b1a1e0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,12 @@ package org.springframework.amqp.rabbit.config; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.MessageListener; @@ -45,6 +47,7 @@ import org.springframework.scheduling.TaskScheduler; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; import org.springframework.util.backoff.BackOff; @@ -54,6 +57,8 @@ * @author Gary Russell * @author Artem Bilan * @author Johno Crawford + * @author Jeonggi Kim + * @author Ngoc Nhan * * @since 2.0 * @@ -61,115 +66,125 @@ public class ListenerContainerFactoryBean extends AbstractFactoryBean implements ApplicationContextAware, BeanNameAware, ApplicationEventPublisherAware, SmartLifecycle { - private ApplicationContext applicationContext; + private final Map micrometerTags = new HashMap<>(); - private String beanName; + private @Nullable ApplicationContext applicationContext; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable String beanName; + + private @Nullable ApplicationEventPublisher applicationEventPublisher; private Type type = Type.simple; - private AbstractMessageListenerContainer listenerContainer; + private @Nullable AbstractMessageListenerContainer listenerContainer; + + private @Nullable ConnectionFactory connectionFactory; + + private @Nullable Boolean channelTransacted; + + private @Nullable AcknowledgeMode acknowledgeMode; - private ConnectionFactory connectionFactory; + private String @Nullable [] queueNames; - private Boolean channelTransacted; + private Queue @Nullable [] queues; - private AcknowledgeMode acknowledgeMode; + private @Nullable Boolean exposeListenerChannel; - private String[] queueNames; + private @Nullable MessageListener messageListener; - private Queue[] queues; + private @Nullable ErrorHandler errorHandler; - private Boolean exposeListenerChannel; + private @Nullable Boolean deBatchingEnabled; - private MessageListener messageListener; + private Advice @Nullable [] adviceChain; - private ErrorHandler errorHandler; + private MessagePostProcessor @Nullable [] afterReceivePostProcessors; - private Boolean deBatchingEnabled; + private @Nullable Boolean autoStartup; - private Advice[] adviceChain; + private @Nullable Integer phase; - private MessagePostProcessor[] afterReceivePostProcessors; + private @Nullable String listenerId; - private Boolean autoStartup; + private @Nullable ConsumerTagStrategy consumerTagStrategy; - private Integer phase; + private @Nullable Map consumerArgs; - private String listenerId; + private @Nullable Boolean noLocal; - private ConsumerTagStrategy consumerTagStrategy; + private @Nullable Boolean exclusive; - private Map consumerArgs; + private @Nullable Boolean defaultRequeueRejected; - private Boolean noLocal; + private @Nullable Integer prefetchCount; - private Boolean exclusive; + private @Nullable Boolean globalQos; - private Boolean defaultRequeueRejected; + private @Nullable Long shutdownTimeout; - private Integer prefetchCount; + private @Nullable Long idleEventInterval; - private Boolean globalQos; + private @Nullable PlatformTransactionManager transactionManager; - private Long shutdownTimeout; + private @Nullable TransactionAttribute transactionAttribute; - private Long idleEventInterval; + private @Nullable Executor taskExecutor; - private PlatformTransactionManager transactionManager; + private @Nullable Long recoveryInterval; - private TransactionAttribute transactionAttribute; + private @Nullable BackOff recoveryBackOff; - private Executor taskExecutor; + private @Nullable MessagePropertiesConverter messagePropertiesConverter; - private Long recoveryInterval; + private @Nullable RabbitAdmin rabbitAdmin; - private BackOff recoveryBackOff; + private @Nullable Boolean missingQueuesFatal; - private MessagePropertiesConverter messagePropertiesConverter; + private @Nullable Boolean possibleAuthenticationFailureFatal; - private RabbitAdmin rabbitAdmin; + private @Nullable Boolean mismatchedQueuesFatal; - private Boolean missingQueuesFatal; + private @Nullable Boolean autoDeclare; - private Boolean possibleAuthenticationFailureFatal; + private @Nullable Long failedDeclarationRetryInterval; - private Boolean mismatchedQueuesFatal; + private @Nullable ConditionalExceptionLogger exclusiveConsumerExceptionLogger; - private Boolean autoDeclare; + private @Nullable Integer consumersPerQueue; - private Long failedDeclarationRetryInterval; + private @Nullable TaskScheduler taskScheduler; - private ConditionalExceptionLogger exclusiveConsumerExceptionLogger; + private @Nullable Long monitorInterval; - private Integer consumersPerQueue; + private @Nullable Integer concurrentConsumers; - private TaskScheduler taskScheduler; + private @Nullable Integer maxConcurrentConsumers; - private Long monitorInterval; + private @Nullable Long startConsumerMinInterval; - private Integer concurrentConsumers; + private @Nullable Long stopConsumerMinInterval; - private Integer maxConcurrentConsumers; + private @Nullable Integer consecutiveActiveTrigger; - private Long startConsumerMinInterval; + private @Nullable Integer consecutiveIdleTrigger; - private Long stopConsumerMinInterval; + private @Nullable Long receiveTimeout; - private Integer consecutiveActiveTrigger; + private @Nullable Long batchReceiveTimeout; - private Integer consecutiveIdleTrigger; + private @Nullable Integer batchSize; - private Long receiveTimeout; + private @Nullable Integer declarationRetries; - private Integer batchSize; + private @Nullable Long retryDeclarationInterval; - private Integer declarationRetries; + private @Nullable Boolean consumerBatchEnabled; - private Long retryDeclarationInterval; + private @Nullable Boolean micrometerEnabled; - private Boolean consumerBatchEnabled; + private @Nullable ContainerCustomizer smlcCustomizer; + + private @Nullable ContainerCustomizer dmlcCustomizer; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { @@ -203,11 +218,11 @@ public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { } public void setQueueNames(String... queueName) { // NOSONAR - this.queueNames = queueName; + this.queueNames = queueName; // NOSONAR } public void setQueues(Queue... queues) { // NOSONAR - this.queues = queues; + this.queues = queues; // NOSONAR } public void setExposeListenerChannel(boolean exposeListenerChannel) { @@ -227,11 +242,11 @@ public void setDeBatchingEnabled(boolean deBatchingEnabled) { } public void setAdviceChain(Advice... adviceChain) { // NOSONAR - this.adviceChain = adviceChain; + this.adviceChain = adviceChain; // NOSONAR } public void setAfterReceivePostProcessors(MessagePostProcessor... afterReceivePostProcessors) { // NOSONAR - this.afterReceivePostProcessors = afterReceivePostProcessors; + this.afterReceivePostProcessors = afterReceivePostProcessors; // NOSONAR } public void setAutoStartup(boolean autoStartup) { @@ -380,6 +395,18 @@ public void setReceiveTimeout(long receiveTimeout) { this.receiveTimeout = receiveTimeout; } + /** + * The number of milliseconds of timeout for gathering batch messages. + * It limits the time to wait to fill batchSize. + * Default is 0 (no timeout). + * @param batchReceiveTimeout the timeout for gathering batch messages. + * @since 3.1.2 + * @see #setBatchSize(int) + */ + public void setBatchReceiveTimeout(long batchReceiveTimeout) { + this.batchReceiveTimeout = batchReceiveTimeout; + } + /** * This property has several functions. *

@@ -427,6 +454,44 @@ public void setRetryDeclarationInterval(long retryDeclarationInterval) { this.retryDeclarationInterval = retryDeclarationInterval; } + /** + * Set to false to disable micrometer listener timers. + * @param enabled false to disable. + * @since 2.4.6 + */ + public void setMicrometerEnabled(boolean enabled) { + this.micrometerEnabled = enabled; + } + + /** + * Set additional tags for the Micrometer listener timers. + * @param tags the tags. + * @since 2.4.6 + */ + public void setMicrometerTags(Map tags) { + this.micrometerTags.putAll(tags); + } + + /** + * Set a {@link ContainerCustomizer} that is invoked after a container is created and + * configured to enable further customization of the container. + * @param customizer the customizer. + * @since 2.4.6 + */ + public void setSMLCCustomizer(ContainerCustomizer customizer) { + this.smlcCustomizer = customizer; + } + + /** + * Set a {@link ContainerCustomizer} that is invoked after a container is created and + * configured to enable further customization of the container. + * @param customizer the customizer. + * @since 2.4.6 + */ + public void setDMLCCustomizer(ContainerCustomizer customizer) { + this.dmlcCustomizer = customizer; + } + @Override public Class getObjectType() { return this.listenerContainer == null @@ -478,7 +543,16 @@ protected AbstractMessageListenerContainer createInstance() { // NOSONAR complex .acceptIfNotNull(this.autoDeclare, container::setAutoDeclare) .acceptIfNotNull(this.failedDeclarationRetryInterval, container::setFailedDeclarationRetryInterval) .acceptIfNotNull(this.exclusiveConsumerExceptionLogger, - container::setExclusiveConsumerExceptionLogger); + container::setExclusiveConsumerExceptionLogger) + .acceptIfNotNull(this.micrometerEnabled, container::setMicrometerEnabled) + .acceptIfCondition(!this.micrometerTags.isEmpty(), this.micrometerTags, + container::setMicrometerTags); + if (this.smlcCustomizer != null && this.type.equals(Type.simple)) { + this.smlcCustomizer.configure((SimpleMessageListenerContainer) container); + } + else if (this.dmlcCustomizer != null && this.type.equals(Type.direct)) { + this.dmlcCustomizer.configure((DirectMessageListenerContainer) container); + } container.afterPropertiesSet(); this.listenerContainer = container; } @@ -486,6 +560,7 @@ protected AbstractMessageListenerContainer createInstance() { // NOSONAR complex } private AbstractMessageListenerContainer createContainer() { + Assert.notNull(this.connectionFactory, "'connectionFactory' is required"); if (this.type.equals(Type.simple)) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); JavaUtils.INSTANCE @@ -496,20 +571,20 @@ private AbstractMessageListenerContainer createContainer() { .acceptIfNotNull(this.consecutiveActiveTrigger, container::setConsecutiveActiveTrigger) .acceptIfNotNull(this.consecutiveIdleTrigger, container::setConsecutiveIdleTrigger) .acceptIfNotNull(this.receiveTimeout, container::setReceiveTimeout) + .acceptIfNotNull(this.batchReceiveTimeout, container::setBatchReceiveTimeout) .acceptIfNotNull(this.batchSize, container::setBatchSize) .acceptIfNotNull(this.consumerBatchEnabled, container::setConsumerBatchEnabled) .acceptIfNotNull(this.declarationRetries, container::setDeclarationRetries) .acceptIfNotNull(this.retryDeclarationInterval, container::setRetryDeclarationInterval); return container; } - else { - DirectMessageListenerContainer container = new DirectMessageListenerContainer(this.connectionFactory); - JavaUtils.INSTANCE - .acceptIfNotNull(this.consumersPerQueue, container::setConsumersPerQueue) - .acceptIfNotNull(this.taskScheduler, container::setTaskScheduler) - .acceptIfNotNull(this.monitorInterval, container::setMonitorInterval); - return container; - } + + DirectMessageListenerContainer container = new DirectMessageListenerContainer(this.connectionFactory); + JavaUtils.INSTANCE + .acceptIfNotNull(this.consumersPerQueue, container::setConsumersPerQueue) + .acceptIfNotNull(this.taskScheduler, container::setTaskScheduler) + .acceptIfNotNull(this.monitorInterval, container::setMonitorInterval); + return container; } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java index d574c87657..b563e591c9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; @@ -42,6 +43,7 @@ /** * @author Mark Fisher * @author Gary Russell + * @author Ngoc Nhan * @since 1.0 */ class ListenerContainerParser implements BeanDefinitionParser { @@ -68,9 +70,9 @@ class ListenerContainerParser implements BeanDefinitionParser { private static final String EXCLUSIVE = "exclusive"; - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) @Override - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element)); parserContext.pushContainingComponent(compositeDef); @@ -96,12 +98,12 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { parserContext.getReaderContext().error("Unexpected configuration for bean " + group, element); } containerList = (ManagedList) constructorArgumentValues - .getIndexedArgumentValue(0, ManagedList.class).getValue(); // NOSONAR never null + .getIndexedArgumentValue(0, ManagedList.class).getValue(); } List childElements = DomUtils.getChildElementsByTagName(element, LISTENER_ELEMENT); - for (int i = 0; i < childElements.size(); i++) { - parseListener(childElements.get(i), element, parserContext, containerList); + for (Element childElement : childElements) { + parseListener(childElement, element, parserContext, containerList); } parserContext.popAndRegisterContainingComponent(); @@ -109,7 +111,8 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { } private void parseListener(Element listenerEle, Element containerEle, ParserContext parserContext, // NOSONAR complexity - ManagedList containerList) { + @Nullable ManagedList containerList) { + RootBeanDefinition listenerDef = new RootBeanDefinition(); listenerDef.setSource(parserContext.extractSource(listenerEle)); @@ -164,7 +167,7 @@ private void parseListener(Element listenerEle, Element containerEle, ParserCont String childElementId = listenerEle.getAttribute(ID_ATTRIBUTE); String containerBeanName = StringUtils.hasText(childElementId) ? childElementId : - BeanDefinitionReaderUtils.generateBeanName(containerDef, parserContext.getRegistry()); + BeanDefinitionReaderUtils.generateBeanName(containerDef, parserContext.getRegistry()); if (!NamespaceUtils.isAttributeDefined(listenerEle, QUEUE_NAMES_ATTRIBUTE) && !NamespaceUtils.isAttributeDefined(listenerEle, QUEUES_ATTRIBUTE)) { @@ -179,31 +182,31 @@ private void parseListener(Element listenerEle, Element containerEle, ParserCont String queueNames = listenerEle.getAttribute(QUEUE_NAMES_ATTRIBUTE); if (StringUtils.hasText(queueNames)) { - String[] names = StringUtils.commaDelimitedListToStringArray(queueNames); - List values = new ManagedList(); - for (int i = 0; i < names.length; i++) { - values.add(new TypedStringValue(names[i].trim())); - } - containerDef.getPropertyValues().add("queueNames", values); + containerDef.getPropertyValues().add("queueNames", queueNames); } String queues = listenerEle.getAttribute(QUEUES_ATTRIBUTE); if (StringUtils.hasText(queues)) { - String[] names = StringUtils.commaDelimitedListToStringArray(queues); - List values = new ManagedList(); - for (int i = 0; i < names.length; i++) { - values.add(new RuntimeBeanReference(names[i].trim())); + if (queues.startsWith("#{")) { + containerDef.getPropertyValues().add("queues", queues); + } + else { + String[] names = StringUtils.commaDelimitedListToStringArray(queues); + List values = new ManagedList<>(); + for (String name : names) { + values.add(new RuntimeBeanReference(name.trim())); + } + containerDef.getPropertyValues().add("queues", values); } - containerDef.getPropertyValues().add("queues", values); } - ManagedMap args = new ManagedMap(); + ManagedMap args = new ManagedMap<>(); String priority = listenerEle.getAttribute("priority"); if (StringUtils.hasText(priority)) { args.put("x-priority", new TypedStringValue(priority, Integer.class)); } - if (args.size() > 0) { + if (!args.isEmpty()) { containerDef.getPropertyValues().add("consumerArguments", args); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java index afdd5ada8a..6ec9d21e0c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.rabbit.support.ExpressionFactoryBean; @@ -37,13 +38,17 @@ * @author Mark Pollack * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * */ public abstract class NamespaceUtils { public static final String BASE_PACKAGE = "org.springframework.amqp.core.rabbit.config"; + public static final String REF_ATTRIBUTE = "ref"; + public static final String METHOD_ATTRIBUTE = "method"; + public static final String ORDER = "order"; /** @@ -83,6 +88,7 @@ public static boolean setValueIfAttributeDefined(BeanDefinitionBuilder builder, */ public static boolean setValueIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + return setValueIfAttributeDefined(builder, element, attributeName, Conventions.attributeNameToPropertyName(attributeName)); } @@ -110,6 +116,7 @@ public static boolean isAttributeDefined(Element element, String attributeName) */ public static boolean addConstructorArgValueIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + String value = element.getAttribute(attributeName); if (StringUtils.hasText(value)) { builder.addConstructorArgValue(new TypedStringValue(value)); @@ -125,10 +132,11 @@ public static boolean addConstructorArgValueIfAttributeDefined(BeanDefinitionBui * @param builder the bean definition builder to be configured * @param element the XML element where the attribute should be defined * @param attributeName the name of the attribute whose value will be used as a constructor argument - * @param defaultValue the default value to use if the attirbute is not set + * @param defaultValue the default value to use if the attribute is not set */ public static void addConstructorArgBooleanValueIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName, boolean defaultValue) { + String value = element.getAttribute(attributeName); if (StringUtils.hasText(value)) { builder.addConstructorArgValue(new TypedStringValue(value)); @@ -150,6 +158,7 @@ public static void addConstructorArgBooleanValueIfAttributeDefined(BeanDefinitio */ public static boolean addConstructorArgRefIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + String value = element.getAttribute(attributeName); if (StringUtils.hasText(value)) { builder.addConstructorArgReference(value); @@ -170,6 +179,7 @@ public static boolean addConstructorArgRefIfAttributeDefined(BeanDefinitionBuild */ public static boolean addConstructorArgParentRefIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + String value = element.getAttribute(attributeName); if (StringUtils.hasText(value)) { BeanDefinitionBuilder child = BeanDefinitionBuilder.genericBeanDefinition(); @@ -193,6 +203,7 @@ public static boolean addConstructorArgParentRefIfAttributeDefined(BeanDefinitio */ public static boolean setReferenceIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName, String propertyName) { + String attributeValue = element.getAttribute(attributeName); if (StringUtils.hasText(attributeValue)) { builder.addPropertyReference(propertyName, attributeValue); @@ -218,12 +229,13 @@ public static boolean setReferenceIfAttributeDefined(BeanDefinitionBuilder build */ public static boolean setReferenceIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + return setReferenceIfAttributeDefined(builder, element, attributeName, Conventions.attributeNameToPropertyName(attributeName)); } /** - * Provides a user friendly description of an element based on its node name and, if available, its "id" attribute + * Provides a user-friendly description of an element based on its node name and, if available, its "id" attribute * value. This is useful for creating error messages from within bean definition parsers. * * @param element The element. @@ -249,7 +261,7 @@ public static void parseDeclarationControls(Element element, BeanDefinitionBuild String admins = element.getAttribute("declared-by"); if (StringUtils.hasText(admins)) { String[] adminBeanNames = admins.split(","); - ManagedList adminBeanRefs = new ManagedList(); + ManagedList adminBeanRefs = new ManagedList<>(); for (String adminBeanName : adminBeanNames) { adminBeanRefs.add(new RuntimeBeanReference(adminBeanName.trim())); } @@ -258,7 +270,7 @@ public static void parseDeclarationControls(Element element, BeanDefinitionBuild NamespaceUtils.setValueIfAttributeDefined(builder, element, "ignore-declaration-exceptions"); } - public static BeanDefinition createExpressionDefinitionFromValueOrExpression(String valueElementName, + public static @Nullable BeanDefinition createExpressionDefinitionFromValueOrExpression(String valueElementName, String expressionElementName, ParserContext parserContext, Element element, boolean oneRequired) { Assert.hasText(valueElementName, "'valueElementName' must not be empty"); @@ -290,7 +302,8 @@ public static BeanDefinition createExpressionDefinitionFromValueOrExpression(Str return expressionDef; } - public static BeanDefinition createExpressionDefIfAttributeDefined(String expressionElementName, Element element) { + public static @Nullable BeanDefinition createExpressionDefIfAttributeDefined( + String expressionElementName, Element element) { Assert.hasText(expressionElementName, "'expressionElementName' must no be empty"); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java index 4bce257e1a..54196783ec 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.core.AnonymousQueue; @@ -33,11 +34,12 @@ * @author Gary Russell * @author Felipe Gutierrez * @author Artem Bilan + * @author Ngoc Nhan * */ public class QueueParser extends AbstractSingleBeanDefinitionParser { - private static final ThreadLocal CURRENT_ELEMENT = new ThreadLocal<>(); + private static final ThreadLocal<@Nullable Element> CURRENT_ELEMENT = new ThreadLocal<>(); /** Element OR attribute. */ private static final String ARGUMENTS = "queue-arguments"; @@ -134,7 +136,7 @@ private void parseArguments(Element element, ParserContext parserContext, BeanDe Map map = parserContext.getDelegate().parseMapElement(argumentsElement, builder.getRawBeanDefinition()); if (StringUtils.hasText(ref)) { - if (map != null && map.size() > 0) { + if (!map.isEmpty()) { parserContext.getReaderContext() .error("You cannot have both a 'ref' and a nested map", element); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java index 69ba403406..759e5a344b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.core.AcknowledgeMode; @@ -357,29 +358,23 @@ public static BeanDefinition parseContainer(Element containerEle, ParserContext return containerDef; } - private static AcknowledgeMode parseAcknowledgeMode(Element ele, ParserContext parserContext) { - AcknowledgeMode acknowledgeMode = null; + private static @Nullable AcknowledgeMode parseAcknowledgeMode(Element ele, ParserContext parserContext) { String acknowledge = ele.getAttribute(ACKNOWLEDGE_ATTRIBUTE); if (StringUtils.hasText(acknowledge)) { - if (ACKNOWLEDGE_AUTO.equals(acknowledge)) { - acknowledgeMode = AcknowledgeMode.AUTO; - } - else if (ACKNOWLEDGE_MANUAL.equals(acknowledge)) { - acknowledgeMode = AcknowledgeMode.MANUAL; - } - else if (ACKNOWLEDGE_NONE.equals(acknowledge)) { - acknowledgeMode = AcknowledgeMode.NONE; - } - else { - parserContext.getReaderContext().error( + return switch (acknowledge) { + case ACKNOWLEDGE_AUTO -> AcknowledgeMode.AUTO; + case ACKNOWLEDGE_MANUAL -> AcknowledgeMode.MANUAL; + case ACKNOWLEDGE_NONE -> AcknowledgeMode.NONE; + default -> { + parserContext.getReaderContext().error( "Invalid listener container 'acknowledge' setting [" + acknowledge - + "]: only \"auto\", \"manual\", and \"none\" supported.", ele); - } - return acknowledgeMode; - } - else { - return null; + + "]: only \"auto\", \"manual\", and \"none\" supported.", ele); + yield null; + } + }; } + + return null; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilder.java index 4b32ee40ae..64ea7afa01 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.rabbit.config; +import java.util.Objects; + import org.aopalliance.intercept.MethodInterceptor; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.retry.MessageKeyGenerator; import org.springframework.amqp.rabbit.retry.MessageRecoverer; @@ -71,13 +74,13 @@ */ public abstract class RetryInterceptorBuilder, T extends MethodInterceptor> { - private RetryOperations retryOperations; + private @Nullable RetryOperations retryOperations; private final RetryTemplate retryTemplate = new RetryTemplate(); private final SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(); - private MessageRecoverer messageRecoverer; + private @Nullable MessageRecoverer messageRecoverer; private boolean templateAltered; @@ -198,17 +201,11 @@ protected void applyCommonSettings(AbstractRetryOperationsInterceptorFactoryBean if (this.messageRecoverer != null) { factoryBean.setMessageRecoverer(this.messageRecoverer); } - if (this.retryOperations != null) { - factoryBean.setRetryOperations(this.retryOperations); - } - else { - factoryBean.setRetryOperations(this.retryTemplate); - } + factoryBean.setRetryOperations(Objects.requireNonNullElse(this.retryOperations, this.retryTemplate)); } public abstract T build(); - /** * Builder for a stateful interceptor. */ @@ -218,9 +215,9 @@ public static final class StatefulRetryInterceptorBuilder private final StatefulRetryOperationsInterceptorFactoryBean factoryBean = new StatefulRetryOperationsInterceptorFactoryBean(); - private MessageKeyGenerator messageKeyGenerator; + private @Nullable MessageKeyGenerator messageKeyGenerator; - private NewMessageIdentifier newMessageIdentifier; + private @Nullable NewMessageIdentifier newMessageIdentifier; StatefulRetryInterceptorBuilder() { } @@ -260,7 +257,6 @@ public StatefulRetryOperationsInterceptor build() { } - /** * Builder for a stateless interceptor. */ diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java index a82840e62c..ba2e09bfa6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.utils.JavaUtils; @@ -32,29 +34,34 @@ * @author Gary Russell * @author Artem Bilan * @author Dustin Schultz + * @author Jeonggi Kim * * @since 1.4 */ public class SimpleRabbitListenerContainerFactory extends AbstractRabbitListenerContainerFactory { - private Integer batchSize; + private @Nullable Integer batchSize; + + private @Nullable Integer concurrentConsumers; - private Integer concurrentConsumers; + private @Nullable Integer maxConcurrentConsumers; - private Integer maxConcurrentConsumers; + private @Nullable Long startConsumerMinInterval; - private Long startConsumerMinInterval; + private @Nullable Long stopConsumerMinInterval; - private Long stopConsumerMinInterval; + private @Nullable Integer consecutiveActiveTrigger; - private Integer consecutiveActiveTrigger; + private @Nullable Integer consecutiveIdleTrigger; - private Integer consecutiveIdleTrigger; + private @Nullable Long receiveTimeout; - private Long receiveTimeout; + private @Nullable Long batchReceiveTimeout; - private Boolean consumerBatchEnabled; + private @Nullable Boolean consumerBatchEnabled; + + private @Nullable Boolean enforceImmediateAckForManual; /** * @param batchSize the batch size. @@ -121,15 +128,45 @@ public void setReceiveTimeout(Long receiveTimeout) { this.receiveTimeout = receiveTimeout; } + /** + * The number of milliseconds of timeout for gathering batch messages. + * It limits the time to wait to fill batchSize. + * Default is 0 (no timeout). + * @param batchReceiveTimeout the timeout for gathering batch messages. + * @since 3.1.2 + * @see SimpleMessageListenerContainer#setBatchReceiveTimeout + * @see #setBatchSize(Integer) + */ + public void setBatchReceiveTimeout(Long batchReceiveTimeout) { + this.batchReceiveTimeout = batchReceiveTimeout; + } + /** * Set to true to present a list of messages based on the {@link #setBatchSize(Integer)}, - * if the listener supports it. + * if the listener supports it. Starting with version 3.0, setting this to true will + * also {@link #setBatchListener(boolean)} to true. * @param consumerBatchEnabled true to create message batches in the container. * @since 2.2 * @see #setBatchSize(Integer) + * @see #setBatchListener(boolean) */ public void setConsumerBatchEnabled(boolean consumerBatchEnabled) { this.consumerBatchEnabled = consumerBatchEnabled; + if (consumerBatchEnabled) { + setBatchListener(true); + } + } + + /** + * Set to {@code true} to enforce {@link com.rabbitmq.client.Channel#basicAck(long, boolean)} + * for {@link org.springframework.amqp.core.AcknowledgeMode#MANUAL} + * when {@link org.springframework.amqp.ImmediateAcknowledgeAmqpException} is thrown. + * This might be a tentative solution to not break behavior for current minor version. + * @param enforceImmediateAckForManual the flag to ack message for MANUAL mode on ImmediateAcknowledgeAmqpException + * @since 3.1.2 + */ + public void setEnforceImmediateAckForManual(Boolean enforceImmediateAckForManual) { + this.enforceImmediateAckForManual = enforceImmediateAckForManual; } @Override @@ -138,27 +175,31 @@ protected SimpleMessageListenerContainer createContainerInstance() { } @Override - protected void initializeContainer(SimpleMessageListenerContainer instance, RabbitListenerEndpoint endpoint) { + protected void initializeContainer(SimpleMessageListenerContainer instance, + @Nullable RabbitListenerEndpoint endpoint) { + super.initializeContainer(instance, endpoint); JavaUtils javaUtils = JavaUtils.INSTANCE - .acceptIfNotNull(this.batchSize, instance::setBatchSize); + .acceptIfNotNull(this.batchSize, instance::setBatchSize); String concurrency = null; if (endpoint != null) { concurrency = endpoint.getConcurrency(); javaUtils.acceptIfNotNull(concurrency, instance::setConcurrency); } javaUtils - .acceptIfCondition(concurrency == null && this.concurrentConsumers != null, this.concurrentConsumers, - instance::setConcurrentConsumers) - .acceptIfCondition((concurrency == null || !(concurrency.contains("-"))) - && this.maxConcurrentConsumers != null, - this.maxConcurrentConsumers, instance::setMaxConcurrentConsumers) - .acceptIfNotNull(this.startConsumerMinInterval, instance::setStartConsumerMinInterval) - .acceptIfNotNull(this.stopConsumerMinInterval, instance::setStopConsumerMinInterval) - .acceptIfNotNull(this.consecutiveActiveTrigger, instance::setConsecutiveActiveTrigger) - .acceptIfNotNull(this.consecutiveIdleTrigger, instance::setConsecutiveIdleTrigger) - .acceptIfNotNull(this.receiveTimeout, instance::setReceiveTimeout); + .acceptIfCondition(concurrency == null && this.concurrentConsumers != null, this.concurrentConsumers, + instance::setConcurrentConsumers) + .acceptIfCondition((concurrency == null || !(concurrency.contains("-"))) + && this.maxConcurrentConsumers != null, + this.maxConcurrentConsumers, instance::setMaxConcurrentConsumers) + .acceptIfNotNull(this.startConsumerMinInterval, instance::setStartConsumerMinInterval) + .acceptIfNotNull(this.stopConsumerMinInterval, instance::setStopConsumerMinInterval) + .acceptIfNotNull(this.consecutiveActiveTrigger, instance::setConsecutiveActiveTrigger) + .acceptIfNotNull(this.consecutiveIdleTrigger, instance::setConsecutiveIdleTrigger) + .acceptIfNotNull(this.receiveTimeout, instance::setReceiveTimeout) + .acceptIfNotNull(this.batchReceiveTimeout, instance::setBatchReceiveTimeout) + .acceptIfNotNull(this.enforceImmediateAckForManual, instance::setEnforceImmediateAckForManual); if (Boolean.TRUE.equals(this.consumerBatchEnabled)) { instance.setConsumerBatchEnabled(true); /* diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpoint.java index c1dcfd6f10..ff7035257c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author 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.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.rabbit.listener.AbstractRabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; @@ -31,7 +33,7 @@ */ public class SimpleRabbitListenerEndpoint extends AbstractRabbitListenerEndpoint { - private MessageListener messageListener; + private @Nullable MessageListener messageListener; /** @@ -47,13 +49,13 @@ public void setMessageListener(MessageListener messageListener) { * @return the {@link MessageListener} to invoke when a message matching * the endpoint is received. */ - public MessageListener getMessageListener() { + public @Nullable MessageListener getMessageListener() { return this.messageListener; } @Override - protected MessageListener createMessageListener(MessageListenerContainer container) { + protected @Nullable MessageListener createMessageListener(MessageListenerContainer container) { return getMessageListener(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java index 2a89750059..4285b39df4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.core.Message; @@ -33,6 +34,7 @@ import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier; import org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor; import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; /** * Convenient factory bean for creating a stateful retry interceptor for use in a message listener container, giving you @@ -47,6 +49,8 @@ * * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan * * @see RetryOperations#execute(org.springframework.retry.RetryCallback, org.springframework.retry.RecoveryCallback, * org.springframework.retry.RetryState) @@ -54,14 +58,14 @@ */ public class StatefulRetryOperationsInterceptorFactoryBean extends AbstractRetryOperationsInterceptorFactoryBean { - private static Log logger = LogFactory.getLog(StatefulRetryOperationsInterceptorFactoryBean.class); + private static final Log LOGGER = LogFactory.getLog(StatefulRetryOperationsInterceptorFactoryBean.class); - private MessageKeyGenerator messageKeyGenerator; + private @Nullable MessageKeyGenerator messageKeyGenerator; - private NewMessageIdentifier newMessageIdentifier; + private @Nullable NewMessageIdentifier newMessageIdentifier; - public void setMessageKeyGenerator(MessageKeyGenerator messageKeyGeneretor) { - this.messageKeyGenerator = messageKeyGeneretor; + public void setMessageKeyGenerator(MessageKeyGenerator messageKeyGenerator) { + this.messageKeyGenerator = messageKeyGenerator; } public void setNewMessageIdentifier(NewMessageIdentifier newMessageIdentifier) { @@ -87,12 +91,12 @@ public StatefulRetryOperationsInterceptor getObject() { private NewMethodArgumentsIdentifier createNewItemIdentifier() { return args -> { Message message = argToMessage(args); + Assert.notNull(message, "The 'args' must not convert to null"); if (StatefulRetryOperationsInterceptorFactoryBean.this.newMessageIdentifier == null) { - return !message.getMessageProperties().isRedelivered(); - } - else { - return StatefulRetryOperationsInterceptorFactoryBean.this.newMessageIdentifier.isNew(message); + return Boolean.FALSE.equals(message.getMessageProperties().isRedelivered()); } + + return StatefulRetryOperationsInterceptorFactoryBean.this.newMessageIdentifier.isNew(message); }; } @@ -102,13 +106,13 @@ private MethodInvocationRecoverer createRecoverer() { MessageRecoverer messageRecoverer = getMessageRecoverer(); Object arg = args[1]; if (messageRecoverer == null) { - logger.warn("Message(s) dropped on recovery: " + arg, cause); + LOGGER.warn("Message(s) dropped on recovery: " + arg, cause); } - else if (arg instanceof Message) { - messageRecoverer.recover((Message) arg, cause); + else if (arg instanceof Message msg) { + messageRecoverer.recover(msg, cause); } - else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer) { - ((MessageBatchRecoverer) messageRecoverer).recover((List) arg, cause); + else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer recoverer) { + recoverer.recover((List) arg, cause); } // This is actually a normal outcome. It means the recovery was successful, but we don't want to consume // any more messages until the acks and commits are sent for this (problematic) message... @@ -120,30 +124,27 @@ else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecovere private MethodArgumentsKeyGenerator createKeyGenerator() { return args -> { Message message = argToMessage(args); + Assert.notNull(message, "The 'args' must not convert to null"); if (StatefulRetryOperationsInterceptorFactoryBean.this.messageKeyGenerator == null) { String messageId = message.getMessageProperties().getMessageId(); - if (messageId == null && message.getMessageProperties().isRedelivered()) { + if (messageId == null && Boolean.TRUE.equals(message.getMessageProperties().isRedelivered())) { message.getMessageProperties().setFinalRetryForMessageWithNoId(true); } return messageId; } - else { - return StatefulRetryOperationsInterceptorFactoryBean.this.messageKeyGenerator.getKey(message); - } + return StatefulRetryOperationsInterceptorFactoryBean.this.messageKeyGenerator.getKey(message); }; } - @SuppressWarnings("unchecked") - private Message argToMessage(Object[] args) { + private @Nullable Message argToMessage(Object[] args) { Object arg = args[1]; - Message message = null; - if (arg instanceof Message) { - message = (Message) arg; + if (arg instanceof Message msg) { + return msg; } - else if (arg instanceof List) { - message = ((List) arg).get(0); + if (arg instanceof List list) { + return (Message) list.get(0); } - return message; + return null; } @Override @@ -151,9 +152,4 @@ public Class getObjectType() { return StatefulRetryOperationsInterceptor.class; } - @Override - public boolean isSingleton() { - return true; - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java index eb6c6b6332..9fb0a4635b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.retry.MessageBatchRecoverer; @@ -36,7 +37,7 @@ * if your listener can be called repeatedly between failures with no side effects. The semantics of stateless retry * mean that a listener exception is not propagated to the container until the retry attempts are exhausted. When the * retry attempts are exhausted it can be processed using a {@link MessageRecoverer} if one is provided, in the same - * transaction (in which case no exception is propagated). If a recoverer is not provided the exception will be + * transaction (in which case no exception is propagated). If a recoverer is not provided, the exception will be * propagated and the message may be redelivered if the channel is transactional. * * @author Dave Syer @@ -46,7 +47,7 @@ */ public class StatelessRetryOperationsInterceptorFactoryBean extends AbstractRetryOperationsInterceptorFactoryBean { - private static Log logger = LogFactory.getLog(StatelessRetryOperationsInterceptorFactoryBean.class); + protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR @Override public RetryOperationsInterceptor getObject() { @@ -62,22 +63,24 @@ public RetryOperationsInterceptor getObject() { } + protected MethodInvocationRecoverer createRecoverer() { + return this::recover; + } + @SuppressWarnings("unchecked") - private MethodInvocationRecoverer createRecoverer() { - return (args, cause) -> { - MessageRecoverer messageRecoverer = getMessageRecoverer(); - Object arg = args[1]; - if (messageRecoverer == null) { - logger.warn("Message(s) dropped on recovery: " + arg, cause); - } - else if (arg instanceof Message) { - messageRecoverer.recover((Message) arg, cause); - } - else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer) { - ((MessageBatchRecoverer) messageRecoverer).recover((List) arg, cause); - } - return null; - }; + protected @Nullable Object recover(Object[] args, Throwable cause) { + MessageRecoverer messageRecoverer = getMessageRecoverer(); + Object arg = args[1]; + if (messageRecoverer == null) { + this.logger.warn("Message(s) dropped on recovery: " + arg, cause); + } + else if (arg instanceof Message message) { + messageRecoverer.recover(message, cause); + } + else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer messageBatchRecoverer) { + messageBatchRecoverer.recover((List) arg, cause); + } + return null; } @Override @@ -85,9 +88,4 @@ public Class getObjectType() { return RetryOperationsInterceptor.class; } - @Override - public boolean isSingleton() { - return true; - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java index 8dea913ba8..372471ea84 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ * @author Dave Syer * @author Gary Russell * @author Artem Bilan + * @author Ngoc Nhan */ class TemplateParser extends AbstractSingleBeanDefinitionParser { @@ -65,7 +66,7 @@ class TemplateParser extends AbstractSingleBeanDefinitionParser { private static final String MANDATORY_ATTRIBUTE = "mandatory"; - private static final String RETURN_CALLBACK_ATTRIBUTE = "return-callback"; + private static final String RETURNS_CALLBACK_ATTRIBUTE = "returns-callback"; private static final String CONFIRM_CALLBACK_ATTRIBUTE = "confirm-callback"; @@ -121,7 +122,7 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit } NamespaceUtils.setValueIfAttributeDefined(builder, element, USE_TEMPORARY_REPLY_QUEUES_ATTRIBUTE); NamespaceUtils.setValueIfAttributeDefined(builder, element, REPLY_ADDRESS_ATTRIBUTE); - NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RETURN_CALLBACK_ATTRIBUTE); + NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RETURNS_CALLBACK_ATTRIBUTE); NamespaceUtils.setReferenceIfAttributeDefined(builder, element, CONFIRM_CALLBACK_ATTRIBUTE); NamespaceUtils.setValueIfAttributeDefined(builder, element, CORRELATION_KEY); NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RETRY_TEMPLATE); @@ -160,18 +161,16 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit BeanDefinition replyContainer = null; Element childElement = null; List childElements = DomUtils.getChildElementsByTagName(element, LISTENER_ELEMENT); - if (childElements.size() > 0) { + if (!childElements.isEmpty()) { childElement = childElements.get(0); } if (childElement != null) { replyContainer = parseListener(childElement, element, parserContext); - if (replyContainer != null) { - replyContainer.getPropertyValues().add("messageListener", - new RuntimeBeanReference(element.getAttribute(ID_ATTRIBUTE))); - String replyContainerName = element.getAttribute(ID_ATTRIBUTE) + ".replyListener"; - parserContext.getRegistry().registerBeanDefinition(replyContainerName, replyContainer); - } + replyContainer.getPropertyValues().add("messageListener", + new RuntimeBeanReference(element.getAttribute(ID_ATTRIBUTE))); + String replyContainerName = element.getAttribute(ID_ATTRIBUTE) + ".replyListener"; + parserContext.getRegistry().registerBeanDefinition(replyContainerName, replyContainer); } if (replyContainer == null && element.hasAttribute(REPLY_QUEUE_ATTRIBUTE)) { parserContext.getReaderContext().error( @@ -192,11 +191,9 @@ else if (replyContainer != null && !element.hasAttribute(REPLY_QUEUE_ATTRIBUTE)) private BeanDefinition parseListener(Element childElement, Element element, ParserContext parserContext) { BeanDefinition replyContainer = RabbitNamespaceUtils.parseContainer(childElement, parserContext); - if (replyContainer != null) { - replyContainer.getPropertyValues().add( - "connectionFactory", - new RuntimeBeanReference(element.getAttribute(CONNECTION_FACTORY_ATTRIBUTE))); - } + replyContainer.getPropertyValues().add( + "connectionFactory", + new RuntimeBeanReference(element.getAttribute(CONNECTION_FACTORY_ATTRIBUTE))); if (element.hasAttribute(REPLY_QUEUE_ATTRIBUTE)) { replyContainer.getPropertyValues().add("queues", new RuntimeBeanReference(element.getAttribute(REPLY_QUEUE_ATTRIBUTE))); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/package-info.java index 7d63279049..aea20aa616 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting the Rabbit XML namespace. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.config; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index 95fb13ba56..281906e38a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,9 +32,21 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import com.rabbitmq.client.Address; +import com.rabbitmq.client.AddressResolver; +import com.rabbitmq.client.BlockedListener; +import com.rabbitmq.client.Method; +import com.rabbitmq.client.Recoverable; +import com.rabbitmq.client.RecoveryListener; +import com.rabbitmq.client.ShutdownListener; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.support.ConditionalExceptionLogger; @@ -46,20 +58,11 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; -import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; - -import com.rabbitmq.client.Address; -import com.rabbitmq.client.AddressResolver; -import com.rabbitmq.client.BlockedListener; -import com.rabbitmq.client.Recoverable; -import com.rabbitmq.client.RecoveryListener; -import com.rabbitmq.client.ShutdownListener; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; +import org.springframework.util.backoff.BackOff; /** * @author Dave Syer @@ -67,6 +70,8 @@ * @author Steve Powell * @author Artem Bilan * @author Will Droste + * @author Christian Tzolov + * @author Salk Lee * */ public abstract class AbstractConnectionFactory implements ConnectionFactory, DisposableBean, BeanNameAware, @@ -79,14 +84,12 @@ public abstract class AbstractConnectionFactory implements ConnectionFactory, Di public enum AddressShuffleMode { /** - * Do not shuffle the addresses before or after opening a connection; attempt - * connections in a fixed order. + * Do not shuffle the addresses before or after opening a connection; attempt connections in a fixed order. */ NONE, /** - * Randomly shuffle the addresses before opening a connection; attempt connections - * in the new order. + * Randomly shuffle the addresses before opening a connection; attempt connections in the new order. */ RANDOM, @@ -105,6 +108,8 @@ public enum AddressShuffleMode { protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR + private final Lock lock = new ReentrantLock(); + private final com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory; private final CompositeConnectionListener connectionListener = new CompositeConnectionListener(); @@ -115,7 +120,7 @@ public enum AddressShuffleMode { private ConditionalExceptionLogger closeExceptionLogger = new DefaultChannelCloseLogger(); - private AbstractConnectionFactory publisherConnectionFactory; + private @Nullable AbstractConnectionFactory publisherConnectionFactory; private RecoveryListener recoveryListener = new RecoveryListener() { @@ -135,32 +140,35 @@ public void handleRecovery(Recoverable recoverable) { }; - private ExecutorService executorService; + private @Nullable ExecutorService executorService; - private List

addresses; + private @Nullable List
addresses; - private AddressShuffleMode addressShuffleMode = AddressShuffleMode.NONE; + private AddressShuffleMode addressShuffleMode = AddressShuffleMode.RANDOM; private int closeTimeout = DEFAULT_CLOSE_TIMEOUT; - private ConnectionNameStrategy connectionNameStrategy = - connectionFactory -> (this.beanName != null ? this.beanName : "SpringAMQP") + - "#" + ObjectUtils.getIdentityHexString(this) + ":" + - this.defaultConnectionNameStrategyCounter.getAndIncrement(); + private ConnectionNameStrategy connectionNameStrategy = connectionFactory -> (this.beanName != null ? this.beanName + : "SpringAMQP") + + "#" + ObjectUtils.getIdentityHexString(this) + ":" + + this.defaultConnectionNameStrategyCounter.getAndIncrement(); - private String beanName; + private @Nullable String beanName; + @SuppressWarnings("NullAway.Init") private ApplicationContext applicationContext; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; - private AddressResolver addressResolver; + private @Nullable AddressResolver addressResolver; private volatile boolean contextStopped; + private @Nullable BackOff connectionCreatingBackOff; + /** - * Create a new AbstractConnectionFactory for the given target ConnectionFactory, - * with no publisher connection factory. + * Create a new AbstractConnectionFactory for the given target ConnectionFactory, with no publisher connection + * factory. * @param rabbitConnectionFactory the target ConnectionFactory */ public AbstractConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) { @@ -169,8 +177,7 @@ public AbstractConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitCon } /** - * Set a custom publisher connection factory; the type does not need to be the same - * as this factory. + * Set a custom publisher connection factory; the type does not need to be the same as this factory. * @param publisherConnectionFactory the factory. * @since 2.3.2 */ @@ -206,13 +213,13 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv } } - protected ApplicationEventPublisher getApplicationEventPublisher() { + protected @Nullable ApplicationEventPublisher getApplicationEventPublisher() { return this.applicationEventPublisher; } @Override public void onApplicationEvent(ContextClosedEvent event) { - if (getApplicationContext().equals(event.getApplicationContext())) { + if (event.getApplicationContext().equals(getApplicationContext())) { this.contextStopped = true; } if (this.publisherConnectionFactory != null) { @@ -234,8 +241,8 @@ public com.rabbitmq.client.ConnectionFactory getRabbitConnectionFactory() { } /** - * Return the user name from the underlying rabbit connection factory. - * @return the user name. + * Return the username from the underlying rabbit connection factory. + * @return the username. * @since 1.6 */ @Override @@ -265,8 +272,8 @@ public void setConnectionThreadFactory(ThreadFactory threadFactory) { } /** - * Set an {@link AddressResolver} to use when creating connections; overrides - * {@link #setAddresses(String)}, {@link #setHost(String)}, and {@link #setPort(int)}. + * Set an {@link AddressResolver} to use when creating connections; overrides {@link #setAddresses(String)}, + * {@link #setHost(String)}, and {@link #setPort(int)}. * @param addressResolver the resolver. * @since 2.1.15 */ @@ -303,6 +310,7 @@ public void setUri(String uri) { } @Override + @Nullable public String getHost() { return this.rabbitConnectionFactory.getHost(); } @@ -334,24 +342,48 @@ public int getPort() { } /** - * Set addresses for clustering. - * This property overrides the host+port properties if not empty. - * @param addresses list of addresses with form "host[:port],..." + * Set addresses for clustering. This property overrides the host+port properties if not empty. + * @param addresses list of addresses in form {@code host[:port]}. + * @since 3.2.1 + */ + public void setAddresses(List addresses) { + Assert.notEmpty(addresses, "Addresses must not be empty"); + setAddresses(String.join(",", addresses)); + } + /** + * Set addresses for clustering. This property overrides the host+port properties if not empty. + * @param addresses list of addresses with form {@code host1[:port1],host2[:port2],...}. */ - public synchronized void setAddresses(String addresses) { - if (StringUtils.hasText(addresses)) { - Address[] addressArray = Address.parseAddresses(addresses); - if (addressArray.length > 0) { - this.addresses = new LinkedList<>(Arrays.asList(addressArray)); - if (this.publisherConnectionFactory != null) { - this.publisherConnectionFactory.setAddresses(addresses); + public void setAddresses(String addresses) { + this.lock.lock(); + try { + if (StringUtils.hasText(addresses)) { + Address[] addressArray = Address.parseAddresses(addresses); + if (addressArray.length > 0) { + this.addresses = new LinkedList<>(Arrays.asList(addressArray)); + if (this.publisherConnectionFactory != null) { + this.publisherConnectionFactory.setAddresses(addresses); + } + return; } - return; } + this.logger.info("setAddresses() called with an empty value, will be using the host+port " + + " or addressResolver properties for connections"); + this.addresses = null; + } + finally { + this.lock.unlock(); + } + } + + protected @Nullable List
getAddresses() throws IOException { + this.lock.lock(); + try { + return this.addressResolver != null ? this.addressResolver.getAddresses() : this.addresses; + } + finally { + this.lock.unlock(); } - this.logger.info("setAddresses() called with an empty value, will be using the host+port " - + " or addressResolver properties for connections"); - this.addresses = null; } /** @@ -428,10 +460,8 @@ public void addChannelListener(ChannelListener listener) { } /** - * Provide an Executor for - * use by the Rabbit ConnectionFactory when creating connections. - * Can either be an ExecutorService or a Spring - * ThreadPoolTaskExecutor, as defined by a <task:executor/> element. + * Provide an Executor for use by the Rabbit ConnectionFactory when creating connections. Can either be an + * ExecutorService or a Spring ThreadPoolTaskExecutor, as defined by a <task:executor/> element. * @param executor The executor. */ public void setExecutor(Executor executor) { @@ -450,14 +480,14 @@ public void setExecutor(Executor executor) { } } - @Nullable - protected ExecutorService getExecutorService() { + protected @Nullable ExecutorService getExecutorService() { return this.executorService; } /** - * How long to wait (milliseconds) for a response to a connection close - * operation from the broker; default 30000 (30 seconds). + * How long to wait (milliseconds) for a response to a connection close operation from the broker; + * default 30000 (30 seconds). + * Also used for {@link com.rabbitmq.client.Channel#waitForConfirms()}. * @param closeTimeout the closeTimeout to set. */ public void setCloseTimeout(int closeTimeout) { @@ -472,8 +502,8 @@ public int getCloseTimeout() { } /** - * Provide a {@link ConnectionNameStrategy} to build the name for the target RabbitMQ connection. - * The {@link #beanName} together with a counter is used by default. + * Provide a {@link ConnectionNameStrategy} to build the name for the target RabbitMQ connection. The + * {@link #beanName} together with a counter is used by default. * @param connectionNameStrategy the {@link ConnectionNameStrategy} to use. * @since 2.0 */ @@ -486,10 +516,10 @@ public void setConnectionNameStrategy(ConnectionNameStrategy connectionNameStrat } /** - * Set the strategy for logging close exceptions; by default, if a channel is closed due to a failed - * passive queue declaration, it is logged at debug level. Normal channel closes (200 OK) are not - * logged. All others are logged at ERROR level (unless access is refused due to an exclusive consumer - * condition, in which case, it is logged at INFO level). + * Set the strategy for logging close exceptions; by default, if a channel is closed due to a failed passive queue + * declaration, it is logged at debug level. Normal channel closes (200 OK) are not logged. All others are logged at + * ERROR level (unless access is refused due to an exclusive consumer condition, in which case, it is logged at + * DEBUG level, since 3.1, previously INFO). * @param closeExceptionLogger the {@link ConditionalExceptionLogger}. * @since 1.5 */ @@ -518,26 +548,10 @@ public void setBeanName(String name) { * @return the bean name or null. * @since 1.7.9 */ - @Nullable - protected String getBeanName() { + protected @Nullable String getBeanName() { return this.beanName; } - /** - * When {@link #setAddresses(String) addresses} are provided and there is more than - * one, set to true to shuffle the list before opening a new connection so that the - * connection to the broker will be attempted in random order. - * @param shuffleAddresses true to shuffle the list. - * @since 2.1.8 - * @see Collections#shuffle(List) - * @deprecated since 2.3 in favor of - * {@link #setAddressShuffleMode(AddressShuffleMode)}. - */ - @Deprecated - public void setShuffleAddresses(boolean shuffleAddresses) { - setAddressShuffleMode(AddressShuffleMode.RANDOM); - } - /** * Set the mode for shuffling addresses. * @param addressShuffleMode the address shuffle mode. @@ -553,20 +567,32 @@ public boolean hasPublisherConnectionFactory() { return this.publisherConnectionFactory != null; } + /** + * Set the backoff strategy for creating connections. This enhancement supports custom + * retry policies within the connection module, particularly useful when the maximum + * channel limit is reached. The {@link SimpleConnection#createChannel(boolean)} method + * utilizes this backoff strategy to gracefully handle such limit exceptions. + * @param backOff the backoff strategy to be applied during connection creation + * @since 3.1.3 + */ + public void setConnectionCreatingBackOff(@Nullable BackOff backOff) { + this.connectionCreatingBackOff = backOff; + } + @Override - public ConnectionFactory getPublisherConnectionFactory() { + public @Nullable ConnectionFactory getPublisherConnectionFactory() { return this.publisherConnectionFactory; } protected final Connection createBareConnection() { try { String connectionName = this.connectionNameStrategy.obtainNewConnectionName(this); - com.rabbitmq.client.Connection rabbitConnection = connect(connectionName); - - Connection connection = new SimpleConnection(rabbitConnection, this.closeTimeout); - if (rabbitConnection instanceof AutorecoveringConnection) { - ((AutorecoveringConnection) rabbitConnection).addRecoveryListener(new RecoveryListener() { + rabbitConnection.addShutdownListener(this); + Connection connection = new SimpleConnection(rabbitConnection, this.closeTimeout, + this.connectionCreatingBackOff == null ? null : this.connectionCreatingBackOff.start()); + if (rabbitConnection instanceof AutorecoveringConnection auto) { + auto.addRecoveryListener(new RecoveryListener() { @Override public void handleRecoveryStarted(Recoverable recoverable) { @@ -588,12 +614,13 @@ public void handleRecovery(Recoverable recoverable) { if (this.logger.isInfoEnabled()) { this.logger.info("Created new connection: " + connectionName + "/" + connection); } - if (this.recoveryListener != null && rabbitConnection instanceof AutorecoveringConnection) { - ((AutorecoveringConnection) rabbitConnection).addRecoveryListener(this.recoveryListener); + if (rabbitConnection instanceof AutorecoveringConnection auto) { + auto.addRecoveryListener(this.recoveryListener); } if (this.applicationEventPublisher != null) { - connection.addBlockedListener(new ConnectionBlockedListener(connection, this.applicationEventPublisher)); + connection + .addBlockedListener(new ConnectionBlockedListener(connection, this.applicationEventPublisher)); } return connection; @@ -605,17 +632,23 @@ public void handleRecovery(Recoverable recoverable) { } } - private synchronized com.rabbitmq.client.Connection connect(String connectionName) + private com.rabbitmq.client.Connection connect(String connectionName) throws IOException, TimeoutException { - if (this.addressResolver != null) { - return connectResolver(connectionName); - } - if (this.addresses != null) { - return connectAddresses(connectionName); + this.lock.lock(); + try { + if (this.addressResolver != null) { + return connectResolver(connectionName); + } + if (this.addresses != null) { + return connectAddresses(connectionName); + } + else { + return connectHostPort(connectionName); + } } - else { - return connectHostPort(connectionName); + finally { + this.lock.unlock(); } } @@ -627,22 +660,29 @@ private com.rabbitmq.client.Connection connectResolver(String connectionName) th connectionName); } - private synchronized com.rabbitmq.client.Connection connectAddresses(String connectionName) + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private com.rabbitmq.client.Connection connectAddresses(String connectionName) throws IOException, TimeoutException { - List
addressesToConnect = new ArrayList<>(this.addresses); - if (addressesToConnect.size() > 1 && AddressShuffleMode.RANDOM.equals(this.addressShuffleMode)) { - Collections.shuffle(addressesToConnect); - } - if (this.logger.isInfoEnabled()) { - this.logger.info("Attempting to connect to: " + addressesToConnect); + this.lock.lock(); + try { + List
addressesToConnect = new ArrayList<>(this.addresses); + if (addressesToConnect.size() > 1 && AddressShuffleMode.RANDOM.equals(this.addressShuffleMode)) { + Collections.shuffle(addressesToConnect); + } + if (this.logger.isInfoEnabled()) { + this.logger.info("Attempting to connect to: " + addressesToConnect); + } + com.rabbitmq.client.Connection connection = this.rabbitConnectionFactory.newConnection(this.executorService, + addressesToConnect, connectionName); + if (addressesToConnect.size() > 1 && AddressShuffleMode.INORDER.equals(this.addressShuffleMode)) { + this.addresses.add(this.addresses.remove(0)); + } + return connection; } - com.rabbitmq.client.Connection connection = this.rabbitConnectionFactory.newConnection(this.executorService, - addressesToConnect, connectionName); - if (addressesToConnect.size() > 1 && AddressShuffleMode.INORDER.equals(this.addressShuffleMode)) { - this.addresses.add(this.addresses.remove(0)); + finally { + this.lock.unlock(); } - return connection; } private com.rabbitmq.client.Connection connectHostPort(String connectionName) throws IOException, TimeoutException { @@ -669,7 +709,11 @@ protected final String getDefaultHostName() { @Override public void shutdownCompleted(ShutdownSignalException cause) { - int protocolClassId = cause.getReason().protocolClassId(); + Method reason = cause.getReason(); + int protocolClassId = RabbitUtils.CONNECTION_PROTOCOL_CLASS_ID_10; + if (reason != null) { + protocolClassId = reason.protocolClassId(); + } if (protocolClassId == RabbitUtils.CHANNEL_PROTOCOL_CLASS_ID_20) { this.closeExceptionLogger.log(this.logger, "Shutdown Signal", cause); getChannelListener().onShutDown(cause); @@ -677,7 +721,6 @@ public void shutdownCompleted(ShutdownSignalException cause) { else if (protocolClassId == RabbitUtils.CONNECTION_PROTOCOL_CLASS_ID_10) { getConnectionListener().onShutDown(cause); } - } @Override @@ -697,16 +740,8 @@ public String toString() { } } - private static final class ConnectionBlockedListener implements BlockedListener { - - private final Connection connection; - - private final ApplicationEventPublisher applicationEventPublisher; - - ConnectionBlockedListener(Connection connection, ApplicationEventPublisher applicationEventPublisher) { - this.connection = connection; - this.applicationEventPublisher = applicationEventPublisher; - } + private record ConnectionBlockedListener(Connection connection, ApplicationEventPublisher applicationEventPublisher) + implements BlockedListener { @Override public void handleBlocked(String reason) { @@ -721,27 +756,22 @@ public void handleUnblocked() { } /** - * Default implementation of {@link ConditionalExceptionLogger} for logging channel - * close exceptions. + * Default implementation of {@link ConditionalExceptionLogger} for logging channel close exceptions. * @since 1.5 */ - private static class DefaultChannelCloseLogger implements ConditionalExceptionLogger { - - DefaultChannelCloseLogger() { - } + public static class DefaultChannelCloseLogger implements ConditionalExceptionLogger { @Override - public void log(Log logger, String message, Throwable t) { - if (t instanceof ShutdownSignalException) { - ShutdownSignalException cause = (ShutdownSignalException) t; + public void log(Log logger, String message, @Nullable Throwable t) { + if (t instanceof ShutdownSignalException cause) { if (RabbitUtils.isPassiveDeclarationChannelClose(cause)) { if (logger.isDebugEnabled()) { logger.debug(message + ": " + cause.getMessage()); } } else if (RabbitUtils.isExclusiveUseChannelClose(cause)) { - if (logger.isInfoEnabled()) { - logger.info(message + ": " + cause.getMessage()); + if (logger.isDebugEnabled()) { + logger.debug(message + ": " + cause.getMessage()); } } else if (!RabbitUtils.isNormalChannelClose(cause)) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java index 01f12c90aa..01399efc67 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,11 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -34,23 +36,27 @@ * @author Artem Bilan * @author Josh Chappelle * @author Gary Russell + * @author Leonardo Ferreira + * @author Ngoc Nhan * @since 1.3 */ public abstract class AbstractRoutingConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, - InitializingBean { + InitializingBean, DisposableBean { private final Map targetConnectionFactories = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); - private final List connectionListeners = new ArrayList(); + private final List connectionListeners = new ArrayList<>(); - private ConnectionFactory defaultTargetConnectionFactory; + private @Nullable ConnectionFactory defaultTargetConnectionFactory; private boolean lenientFallback = true; - private Boolean confirms; + private @Nullable Boolean confirms; + + private @Nullable Boolean returns; - private Boolean returns; + private boolean consistentConfirmsReturns = true; /** * Specify the map of target ConnectionFactories, with the lookup key as key. @@ -64,7 +70,7 @@ public void setTargetConnectionFactories(Map targetCo Assert.noNullElements(targetConnectionFactories.values().toArray(), "'targetConnectionFactories' cannot have null values."); this.targetConnectionFactories.putAll(targetConnectionFactories); - targetConnectionFactories.values().stream().forEach(cf -> checkConfirmsAndReturns(cf)); + targetConnectionFactories.values().forEach(this::checkConfirmsAndReturns); } /** @@ -103,12 +109,12 @@ public boolean isLenientFallback() { @Override public boolean isPublisherConfirms() { - return this.confirms; + return Boolean.TRUE.equals(this.confirms); } @Override public boolean isPublisherReturns() { - return this.returns; + return Boolean.TRUE.equals(this.returns); } @Override @@ -123,10 +129,13 @@ private void checkConfirmsAndReturns(ConnectionFactory cf) { if (this.returns == null) { this.returns = cf.isPublisherReturns(); } - Assert.isTrue(this.confirms.booleanValue() == cf.isPublisherConfirms(), - "Target connection factories must have the same setting for publisher confirms"); - Assert.isTrue(this.returns.booleanValue() == cf.isPublisherReturns(), - "Target connection factories must have the same setting for publisher returns"); + + if (this.consistentConfirmsReturns) { + Assert.isTrue(this.confirms == cf.isPublisherConfirms(), + "Target connection factories must have the same setting for publisher confirms"); + Assert.isTrue(this.returns == cf.isPublisherReturns(), + "Target connection factories must have the same setting for publisher returns"); + } } @Override @@ -204,30 +213,49 @@ public void clearConnectionListeners() { } @Override - public String getHost() { - return this.determineTargetConnectionFactory().getHost(); + public @Nullable String getHost() { + return determineTargetConnectionFactory().getHost(); } @Override public int getPort() { - return this.determineTargetConnectionFactory().getPort(); + return determineTargetConnectionFactory().getPort(); } @Override public String getVirtualHost() { - return this.determineTargetConnectionFactory().getVirtualHost(); + return determineTargetConnectionFactory().getVirtualHost(); } @Override public String getUsername() { - return this.determineTargetConnectionFactory().getUsername(); + return determineTargetConnectionFactory().getUsername(); } @Override - public ConnectionFactory getTargetConnectionFactory(Object key) { + public @Nullable ConnectionFactory getTargetConnectionFactory(Object key) { return this.targetConnectionFactories.get(key); } + /** + * Specify whether to apply a validation enforcing all {@link ConnectionFactory#isPublisherConfirms()} and + * {@link ConnectionFactory#isPublisherReturns()} have a consistent value. + *

+ * A consistent value means that all ConnectionFactories must have the same value between all + * {@link ConnectionFactory#isPublisherConfirms()} and the same value between all + * {@link ConnectionFactory#isPublisherReturns()}. + *

+ *

+ * Note that in any case the values between {@link ConnectionFactory#isPublisherConfirms()} and + * {@link ConnectionFactory#isPublisherReturns()} don't need to be equals between each other. + *

+ * @param consistentConfirmsReturns true to validate, false to not validate. + * @since 2.4.4 + */ + public void setConsistentConfirmsReturns(boolean consistentConfirmsReturns) { + this.consistentConfirmsReturns = consistentConfirmsReturns; + } + /** * Adds the given {@link ConnectionFactory} and associates it with the given lookup key. * @param key the lookup key. @@ -238,6 +266,8 @@ protected void addTargetConnectionFactory(Object key, ConnectionFactory connecti for (ConnectionListener listener : this.connectionListeners) { connectionFactory.addConnectionListener(listener); } + + checkConfirmsAndReturns(connectionFactory); } /** @@ -254,7 +284,19 @@ protected ConnectionFactory removeTargetConnectionFactory(Object key) { * * @return The lookup key. */ - @Nullable - protected abstract Object determineCurrentLookupKey(); + protected abstract @Nullable Object determineCurrentLookupKey(); + + @Override + public void destroy() { + resetConnection(); + } + + @Override + public void resetConnection() { + this.targetConnectionFactories.values().forEach(ConnectionFactory::resetConnection); + if (this.defaultTargetConnectionFactory != null) { + this.defaultTargetConnectionFactory.resetConnection(); + } + } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AfterCompletionFailedException.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AfterCompletionFailedException.java index 259f0d5958..18b507f959 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AfterCompletionFailedException.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AfterCompletionFailedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or 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.amqp.rabbit.connection; +import java.io.Serial; + import org.springframework.amqp.AmqpException; /** @@ -27,6 +29,7 @@ */ public class AfterCompletionFailedException extends AmqpException { + @Serial private static final long serialVersionUID = 1L; private final int syncStatus; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 0fa960684b..8ee1f34076 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.net.URI; import java.util.Arrays; import java.util.Collection; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -44,27 +45,32 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.rabbitmq.client.Address; +import com.rabbitmq.client.AlreadyClosedException; +import com.rabbitmq.client.BlockedListener; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ShutdownListener; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpException; import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.rabbit.support.ActiveObjectCounter; import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.SmartLifecycle; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import com.rabbitmq.client.AlreadyClosedException; -import com.rabbitmq.client.BlockedListener; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ShutdownListener; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; - /** * A {@link ConnectionFactory} implementation that (when the cache mode is {@link CacheMode#CHANNEL} (default) * returns the same Connection from all {@link #createConnection()} @@ -82,7 +88,7 @@ *

* {@link CacheMode#CONNECTION} is not compatible with a Rabbit Admin that auto-declares queues etc. *

- * NOTE: This ConnectionFactory requires explicit closing of all Channels obtained form its Connection(s). + * NOTE: This ConnectionFactory requires explicit closing of all Channels obtained from its Connection(s). * This is the usual recommendation for native Rabbit access code anyway. However, with this ConnectionFactory, its use * is mandatory in order to actually allow for Channel reuse. {@link Channel#close()} returns the channel to the * cache, if there is room, or physically closes the channel otherwise. @@ -94,10 +100,11 @@ * @author Artem Bilan * @author Steve Powell * @author Will Droste + * @author Leonardo Ferreira */ @ManagedResource public class CachingConnectionFactory extends AbstractConnectionFactory - implements InitializingBean, ShutdownListener { + implements InitializingBean, ShutdownListener, SmartLifecycle { private static final String UNUSED = "unused"; @@ -112,13 +119,11 @@ public class CachingConnectionFactory extends AbstractConnectionFactory */ private static final AtomicInteger threadPoolId = new AtomicInteger(); // NOSONAR lower case - private static final Set txStarts = new HashSet<>(Arrays.asList("basicPublish", "basicAck", // NOSONAR - "basicNack", "basicReject")); + private static final Set txStarts = Set.of("basicPublish", "basicAck", "basicNack", "basicReject"); - private static final Set ackMethods = new HashSet<>(Arrays.asList("basicAck", // NOSONAR - "basicNack", "basicReject")); + private static final Set ackMethods = Set.of("basicAck", "basicNack", "basicReject"); - private static final Set txEnds = new HashSet<>(Arrays.asList("txCommit", "txRollback")); // NOSONAR + private static final Set txEnds = Set.of("txCommit", "txRollback"); private final ChannelCachingConnectionProxy connection = new ChannelCachingConnectionProxy(null); @@ -145,14 +150,13 @@ public enum CacheMode { public enum ConfirmType { /** - * Use {@code RabbitTemplate#waitForConfirms()} (or {@code waitForConfirmsOrDie()} + * Use {@code RabbitTemplate#waitForConfirms()} or {@code waitForConfirmsOrDie()} * within scoped operations. */ SIMPLE, /** - * Use with {@code CorrelationData} to correlate confirmations with sent - * messsages. + * Use with {@code CorrelationData} to correlate confirmations with sent messages. */ CORRELATED, @@ -173,9 +177,9 @@ public enum ConfirmType { private final BlockingDeque idleConnections = new LinkedBlockingDeque<>(); - private final LinkedList cachedChannelsNonTransactional = new LinkedList<>(); // NOSONAR removeFirst() + private final Deque cachedChannelsNonTransactional = new LinkedList<>(); // NOSONAR removeFirst() - private final LinkedList cachedChannelsTransactional = new LinkedList<>(); // NOSONAR removeFirst() + private final Deque cachedChannelsTransactional = new LinkedList<>(); // NOSONAR removeFirst() private final Map checkoutPermits = new HashMap<>(); @@ -183,11 +187,17 @@ public enum ConfirmType { private final AtomicInteger connectionHighWaterMark = new AtomicInteger(); - /** Synchronization monitor for the shared Connection. */ - private final Object connectionMonitor = new Object(); + /** + * Synchronization lock for the shared Connection. + */ + private final Lock connectionLock = new ReentrantLock(); + + private final Condition connectionAvailableCondition = this.connectionLock.newCondition(); private final ActiveObjectCounter inFlightAsyncCloses = new ActiveObjectCounter<>(); + private final AtomicBoolean running = new AtomicBoolean(); + private long channelCheckoutTimeout = 0; private CacheMode cacheMode = CacheMode.CHANNEL; @@ -213,7 +223,7 @@ public enum ConfirmType { /** * Executor used for channels if no explicit executor set. */ - private volatile ExecutorService channelsExecutor; + private volatile @Nullable ExecutorService channelsExecutor; private volatile boolean stopped; @@ -248,6 +258,7 @@ public CachingConnectionFactory(int port) { * @param hostNameArg the host name to connect to * @param port the port number */ + @SuppressWarnings("this-escape") public CachingConnectionFactory(@Nullable String hostNameArg, int port) { super(newRabbitConnectionFactory()); String hostname = hostNameArg; @@ -264,6 +275,7 @@ public CachingConnectionFactory(@Nullable String hostNameArg, int port) { * @param uri the amqp uri configuring the connection * @since 1.5 */ + @SuppressWarnings("this-escape") public CachingConnectionFactory(URI uri) { super(newRabbitConnectionFactory()); setUri(uri); @@ -283,6 +295,7 @@ public CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConn * @param rabbitConnectionFactory the target ConnectionFactory * @param isPublisherFactory true if this is the publisher sub-factory. */ + @SuppressWarnings("this-escape") private CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory, boolean isPublisherFactory) { @@ -290,14 +303,16 @@ private CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitCon if (!isPublisherFactory) { if (rabbitConnectionFactory.isAutomaticRecoveryEnabled()) { rabbitConnectionFactory.setAutomaticRecoveryEnabled(false); - logger.warn("***\nAutomatic Recovery was Enabled in the provided connection factory;\n" - + "while Spring AMQP is generally compatible with this feature, there\n" - + "are some corner cases where problems arise. Spring AMQP\n" - + "prefers to use its own recovery mechanisms; when this option is true, you may receive\n" - + "'AutoRecoverConnectionNotCurrentlyOpenException's until the connection is recovered.\n" - + "It has therefore been disabled; if you really wish to enable it, use\n" - + "'getRabbitConnectionFactory().setAutomaticRecoveryEnabled(true)',\n" - + "but this is discouraged."); + logger.warn(""" + *** + Automatic Recovery was Enabled in the provided connection factory; + while Spring AMQP is generally compatible with this feature, there + are some corner cases where problems arise. Spring AMQP + prefers to use its own recovery mechanisms; when this option is true, you may receive + 'AutoRecoverConnectionNotCurrentlyOpenException's until the connection is recovered. + It has therefore been disabled; if you really wish to enable it, use + 'getRabbitConnectionFactory().setAutomaticRecoveryEnabled(true)', + but this is discouraged."""); } super.setPublisherConnectionFactory(new CachingConnectionFactory(getRabbitConnectionFactory(), true)); } @@ -326,6 +341,7 @@ public void setPublisherConnectionFactory(@Nullable AbstractConnectionFactory pu * @param sessionCacheSize the channel cache size. * @see #setChannelCheckoutTimeout(long) */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setChannelCacheSize(int sessionCacheSize) { Assert.isTrue(sessionCacheSize >= 1, "Channel cache size must be 1 or higher"); this.channelCacheSize = sessionCacheSize; @@ -342,6 +358,7 @@ public CacheMode getCacheMode() { return this.cacheMode; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setCacheMode(CacheMode cacheMode) { Assert.isTrue(!this.initialized, "'cacheMode' cannot be changed after initialization."); Assert.notNull(cacheMode, "'cacheMode' must not be null."); @@ -355,6 +372,7 @@ public int getConnectionCacheSize() { return this.connectionCacheSize; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setConnectionCacheSize(int connectionCacheSize) { Assert.isTrue(connectionCacheSize >= 1, "Connection cache size must be 1 or higher."); this.connectionCacheSize = connectionCacheSize; @@ -371,6 +389,7 @@ public void setConnectionCacheSize(int connectionCacheSize) { * @param connectionLimit the limit. * @since 1.5.5 */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setConnectionLimit(int connectionLimit) { Assert.isTrue(connectionLimit >= 1, "Connection limit must be 1 or higher."); this.connectionLimit = connectionLimit; @@ -389,6 +408,7 @@ public boolean isPublisherReturns() { return this.publisherReturns; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setPublisherReturns(boolean publisherReturns) { this.publisherReturns = publisherReturns; if (this.defaultPublisherFactory) { @@ -396,45 +416,6 @@ public void setPublisherReturns(boolean publisherReturns) { } } - /** - * Use full (correlated) publisher confirms, with correlation data and a callback for - * each message. - * @param publisherConfirms true for full publisher returns, - * @since 1.1 - * @deprecated in favor of {@link #setPublisherConfirmType(ConfirmType)}. - * @see #setSimplePublisherConfirms(boolean) - */ - @Deprecated - public void setPublisherConfirms(boolean publisherConfirms) { - Assert.isTrue(!publisherConfirms || !ConfirmType.SIMPLE.equals(this.confirmType), - "Cannot set both publisherConfirms and simplePublisherConfirms"); - if (publisherConfirms) { - setPublisherConfirmType(ConfirmType.CORRELATED); - } - else if (this.confirmType.equals(ConfirmType.CORRELATED)) { - setPublisherConfirmType(ConfirmType.NONE); - } - } - - /** - * Use simple publisher confirms where the template simply waits for completion. - * @param simplePublisherConfirms true for confirms. - * @since 2.1 - * @deprecated in favor of {@link #setPublisherConfirmType(ConfirmType)}. - * @see #setPublisherConfirms(boolean) - */ - @Deprecated - public void setSimplePublisherConfirms(boolean simplePublisherConfirms) { - Assert.isTrue(!simplePublisherConfirms || !ConfirmType.CORRELATED.equals(this.confirmType), - "Cannot set both publisherConfirms and simplePublisherConfirms"); - if (simplePublisherConfirms) { - setPublisherConfirmType(ConfirmType.SIMPLE); - } - else if (this.confirmType.equals(ConfirmType.SIMPLE)) { - setPublisherConfirmType(ConfirmType.NONE); - } - } - @Override public boolean isSimplePublisherConfirms() { return this.confirmType.equals(ConfirmType.SIMPLE); @@ -445,6 +426,7 @@ public boolean isSimplePublisherConfirms() { * @param confirmType the confirm type. * @since 2.2 */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setPublisherConfirmType(ConfirmType confirmType) { Assert.notNull(confirmType, "'confirmType' cannot be null"); this.confirmType = confirmType; @@ -465,11 +447,12 @@ public void setPublisherConfirmType(ConfirmType confirmType) { * @since 1.4.2 * @see #setConnectionLimit(int) */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setChannelCheckoutTimeout(long channelCheckoutTimeout) { this.channelCheckoutTimeout = channelCheckoutTimeout; if (this.defaultPublisherFactory) { ((CachingConnectionFactory) getPublisherConnectionFactory()) - .setChannelCheckoutTimeout(channelCheckoutTimeout); // NOSONAR + .setChannelCheckoutTimeout(channelCheckoutTimeout); } } @@ -484,6 +467,12 @@ public void setPublisherChannelFactory(PublisherCallbackChannelFactory publisher } @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + + @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void afterPropertiesSet() { this.initialized = true; if (this.cacheMode == CacheMode.CHANNEL) { @@ -496,6 +485,22 @@ public void afterPropertiesSet() { } } + @Override + public void start() { + this.running.set(true); + } + + @Override + public void stop() { + this.running.set(false); + resetConnection(); + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + private void initCacheWaterMarks() { this.channelHighWaterMarks.put(ObjectUtils.getIdentityHexString(this.cachedChannelsNonTransactional), new AtomicInteger()); @@ -526,12 +531,12 @@ private Channel getChannel(ChannelCachingConnectionProxy connection, boolean tra if (this.channelCheckoutTimeout > 0) { permits = obtainPermits(connection); } - LinkedList channelList = determineChannelList(connection, transactional); + Deque channelList = determineChannelList(connection, transactional); ChannelProxy channel = null; if (connection.isOpen()) { - channel = findOpenChannel(channelList); + channel = findOpenChannel(channelList, connection.channelListLock); if (channel != null && logger.isTraceEnabled()) { - logger.trace("Found cached Rabbit Channel: " + channel.toString()); + logger.trace("Found cached Rabbit Channel: " + channel); } } if (channel == null) { @@ -577,10 +582,10 @@ private Semaphore obtainPermits(ChannelCachingConnectionProxy connection) { } @Nullable - private ChannelProxy findOpenChannel(LinkedList channelList) { // NOSONAR - LL Vs. L - removeFirst() - + private ChannelProxy findOpenChannel(Deque channelList, Lock channelListLock) { ChannelProxy channel = null; - synchronized (channelList) { + channelListLock.lock(); + try { while (!channelList.isEmpty()) { channel = channelList.removeFirst(); if (logger.isTraceEnabled()) { @@ -595,6 +600,9 @@ private ChannelProxy findOpenChannel(LinkedList channelList) { // } } } + finally { + channelListLock.unlock(); + } return channel; } @@ -626,12 +634,10 @@ private void cleanUpClosedChannel(ChannelProxy channel) { } } - private LinkedList determineChannelList(ChannelCachingConnectionProxy connection, // NOSONAR LL - boolean transactional) { - LinkedList channelList; // NOSONAR must be LinkedList + private Deque determineChannelList(ChannelCachingConnectionProxy connection, boolean transactional) { + Deque channelList; if (this.cacheMode == CacheMode.CHANNEL) { - channelList = transactional ? this.cachedChannelsTransactional - : this.cachedChannelsNonTransactional; + channelList = transactional ? this.cachedChannelsTransactional : this.cachedChannelsNonTransactional; } else { channelList = transactional ? this.allocatedConnectionTransactionalChannels.get(connection) @@ -644,7 +650,7 @@ private LinkedList determineChannelList(ChannelCachingConnectionPr } private ChannelProxy getCachedChannelProxy(ChannelCachingConnectionProxy connection, - LinkedList channelList, boolean transactional) { //NOSONAR LinkedList for addLast() + Deque channelList, boolean transactional) { Channel targetChannel = createBareChannel(connection, transactional); if (logger.isDebugEnabled()) { @@ -653,10 +659,10 @@ private ChannelProxy getCachedChannelProxy(ChannelCachingConnectionProxy connect getChannelListener().onCreate(targetChannel, transactional); Class[] interfaces; if (ConfirmType.CORRELATED.equals(this.confirmType) || this.publisherReturns) { - interfaces = new Class[] { ChannelProxy.class, PublisherCallbackChannel.class }; + interfaces = new Class[] {ChannelProxy.class, PublisherCallbackChannel.class}; } else { - interfaces = new Class[] { ChannelProxy.class }; + interfaces = new Class[] {ChannelProxy.class}; } return (ChannelProxy) Proxy.newProxyInstance(ChannelProxy.class.getClassLoader(), interfaces, new CachedChannelInvocationHandler(connection, targetChannel, channelList, @@ -666,7 +672,8 @@ interfaces, new CachedChannelInvocationHandler(connection, targetChannel, channe private Channel createBareChannel(ChannelCachingConnectionProxy connection, boolean transactional) { if (this.cacheMode == CacheMode.CHANNEL) { if (!this.connection.isOpen()) { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (!this.connection.isOpen()) { this.connection.notifyCloseIfNecessary(); } @@ -675,21 +682,34 @@ private Channel createBareChannel(ChannelCachingConnectionProxy connection, bool createConnection(); } } + finally { + this.connectionLock.unlock(); + } } return doCreateBareChannel(this.connection, transactional); } - else if (this.cacheMode == CacheMode.CONNECTION) { + else { if (!connection.isOpen()) { - synchronized (this.connectionMonitor) { - this.allocatedConnectionNonTransactionalChannels.get(connection).clear(); - this.allocatedConnectionTransactionalChannels.get(connection).clear(); + this.connectionLock.lock(); + try { + LinkedList channelProxies = + this.allocatedConnectionNonTransactionalChannels.get(connection); + if (channelProxies != null) { + channelProxies.clear(); + } + channelProxies = this.allocatedConnectionTransactionalChannels.get(connection); + if (channelProxies != null) { + channelProxies.clear(); + } connection.notifyCloseIfNecessary(); refreshProxyConnection(connection); } + finally { + this.connectionLock.unlock(); + } } return doCreateBareChannel(connection, transactional); } - return null; // NOSONAR doCreate will throw an exception } private Channel doCreateBareChannel(ChannelCachingConnectionProxy conn, boolean transactional) { @@ -706,9 +726,7 @@ private Channel doCreateBareChannel(ChannelCachingConnectionProxy conn, boolean && !(channel instanceof PublisherCallbackChannelImpl)) { channel = this.publisherChannelFactory.createChannel(channel, getChannelsExecutor()); } - if (channel != null) { - channel.addShutdownListener(this); - } + channel.addShutdownListener(this); return channel; // NOSONAR - Simple connection throws exception } @@ -718,24 +736,25 @@ public final Connection createConnection() throws AmqpException { throw new AmqpApplicationContextClosedException( "The ApplicationContext is closed and the ConnectionFactory can no longer create connections."); } - synchronized (this.connectionMonitor) { - if (this.cacheMode == CacheMode.CHANNEL) { - if (this.connection.target == null) { - this.connection.target = super.createBareConnection(); - // invoke the listener *after* this.connection is assigned - if (!this.checkoutPermits.containsKey(this.connection)) { - this.checkoutPermits.put(this.connection, new Semaphore(this.channelCacheSize)); - } - this.connection.closeNotified.set(false); - getConnectionListener().onCreate(this.connection); - } - return this.connection; - } - else if (this.cacheMode == CacheMode.CONNECTION) { + this.connectionLock.lock(); + try { + if (this.cacheMode == CacheMode.CONNECTION) { return connectionFromCache(); } + if (this.connection.target == null) { + this.connection.target = super.createBareConnection(); + // invoke the listener *after* this.connection is assigned + if (!this.checkoutPermits.containsKey(this.connection)) { + this.checkoutPermits.put(this.connection, new Semaphore(this.channelCacheSize)); + } + this.connection.closeNotified.set(false); + getConnectionListener().onCreate(this.connection); + } + return this.connection; + } + finally { + this.connectionLock.unlock(); } - return null; // NOSONAR - never reach here - exceptions } private Connection connectionFromCache() { @@ -754,10 +773,10 @@ private Connection connectionFromCache() { logger.debug("Adding new connection '" + cachedConnection + "'"); } this.allocatedConnections.add(cachedConnection); - this.allocatedConnectionNonTransactionalChannels.put(cachedConnection, new LinkedList()); + this.allocatedConnectionNonTransactionalChannels.put(cachedConnection, new LinkedList<>()); this.channelHighWaterMarks.put(ObjectUtils.getIdentityHexString( this.allocatedConnectionNonTransactionalChannels.get(cachedConnection)), new AtomicInteger()); - this.allocatedConnectionTransactionalChannels.put(cachedConnection, new LinkedList()); + this.allocatedConnectionTransactionalChannels.put(cachedConnection, new LinkedList<>()); this.channelHighWaterMarks.put( ObjectUtils .getIdentityHexString(this.allocatedConnectionTransactionalChannels.get(cachedConnection)), @@ -787,8 +806,9 @@ private ChannelCachingConnectionProxy waitForConnection(long now) { while (cachedConnection == null && System.currentTimeMillis() - now < this.channelCheckoutTimeout) { if (countOpenConnections() >= this.connectionLimit) { try { - this.connectionMonitor.wait(this.channelCheckoutTimeout); - cachedConnection = findIdleConnection(); + if (this.connectionAvailableCondition.await(this.channelCheckoutTimeout, TimeUnit.MILLISECONDS)) { + cachedConnection = findIdleConnection(); + } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -804,8 +824,7 @@ private ChannelCachingConnectionProxy waitForConnection(long now) { * return null, if there are no open idle, return the first closed idle so it can * be reopened. */ - @Nullable - private ChannelCachingConnectionProxy findIdleConnection() { + private @Nullable ChannelCachingConnectionProxy findIdleConnection() { ChannelCachingConnectionProxy cachedConnection = null; ChannelCachingConnectionProxy lastIdle = this.idleConnections.peekLast(); while (cachedConnection == null) { @@ -818,7 +837,7 @@ private ChannelCachingConnectionProxy findIdleConnection() { cachedConnection.notifyCloseIfNecessary(); this.idleConnections.addLast(cachedConnection); if (cachedConnection.equals(lastIdle)) { - // all of the idle connections are closed. + // all the idled connections are closed. cachedConnection = this.idleConnections.poll(); break; } @@ -855,25 +874,36 @@ private void refreshProxyConnection(ChannelCachingConnectionProxy connection) { */ @Override public final void destroy() { - super.destroy(); - resetConnection(); if (getContextStopped()) { this.stopped = true; - if (this.channelsExecutor != null) { - try { - if (!this.inFlightAsyncCloses.await(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { - this.logger.warn("Async closes are still in-flight: " + this.inFlightAsyncCloses.getCount()); + this.connectionLock.lock(); + try { + ExecutorService executorService = this.channelsExecutor; + if (executorService != null) { + try { + if (!this.inFlightAsyncCloses.await(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { + this.logger + .warn("Async closes are still in-flight: " + this.inFlightAsyncCloses.getCount()); + } + executorService.shutdown(); + if (!executorService.awaitTermination(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { + this.logger.warn("Channel executor failed to shut down"); + } + } + catch (@SuppressWarnings(UNUSED) InterruptedException e) { + Thread.currentThread().interrupt(); } - this.channelsExecutor.shutdown(); - if (!this.channelsExecutor.awaitTermination(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { - this.logger.warn("Channel executor failed to shut down"); + finally { + this.channelsExecutor = null; } } - catch (@SuppressWarnings(UNUSED) InterruptedException e) { - Thread.currentThread().interrupt(); - } + } + finally { + this.connectionLock.unlock(); } } + super.destroy(); + resetConnection(); } /** @@ -882,24 +912,30 @@ public final void destroy() { * used to force a reconnect to the primary broker after failing over to a secondary * broker. */ + @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void resetConnection() { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.connection.target != null) { this.connection.destroy(); } - this.allocatedConnections.forEach(c -> c.destroy()); + this.allocatedConnections.forEach(ChannelCachingConnectionProxy::destroy); this.channelHighWaterMarks.values().forEach(count -> count.set(0)); this.connectionHighWaterMark.set(0); } + finally { + this.connectionLock.unlock(); + } if (this.defaultPublisherFactory) { - ((CachingConnectionFactory) getPublisherConnectionFactory()).resetConnection(); // NOSONAR + getPublisherConnectionFactory().resetConnection(); // NOSONAR } } /* * Reset the Channel cache and underlying shared Connection, to be reinitialized on next access. */ - protected void reset(List channels, List txChannels, + protected void reset(Deque channels, Deque txChannels, Map channelsAwaitingAcks) { this.active = false; @@ -911,10 +947,8 @@ protected void reset(List channels, List txChannels, } protected void closeAndClear(Collection theChannels) { - synchronized (theChannels) { - closeChannels(theChannels); - theChannels.clear(); - } + closeChannels(theChannels); + theChannels.clear(); } protected void closeChannels(Collection theChannels) { @@ -929,10 +963,12 @@ protected void closeChannels(Collection theChannels) { } @ManagedAttribute + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Properties getCacheProperties() { Properties props = new Properties(); props.setProperty("cacheMode", this.cacheMode.name()); - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { props.setProperty("channelCacheSize", Integer.toString(this.channelCacheSize)); if (this.cacheMode.equals(CacheMode.CONNECTION)) { props.setProperty("connectionCacheSize", Integer.toString(this.connectionCacheSize)); @@ -975,6 +1011,9 @@ public Properties getCacheProperties() { putConnectionName(props, this.connection, ""); } } + finally { + this.connectionLock.unlock(); + } return props; } @@ -984,6 +1023,7 @@ public Properties getCacheProperties() { * @since 2.0.2 */ @ManagedAttribute + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Properties getPublisherConnectionFactoryCacheProperties() { if (this.defaultPublisherFactory) { return ((CachingConnectionFactory) getPublisherConnectionFactory()).getCacheProperties(); // NOSONAR @@ -992,14 +1032,12 @@ public Properties getPublisherConnectionFactoryCacheProperties() { } private void putConnectionName(Properties props, ConnectionProxy connection, String keySuffix) { - Connection targetConnection = connection.getTargetConnection(); // NOSONAR (close()) + Connection targetConnection = connection.getTargetConnection(); if (targetConnection != null) { com.rabbitmq.client.Connection delegate = targetConnection.getDelegate(); - if (delegate != null) { - String name = delegate.getClientProvidedName(); - if (name != null) { - props.put("connectionName" + keySuffix, name); - } + String name = delegate.getClientProvidedName(); + if (name != null) { + props.put("connectionName" + keySuffix, name); } } } @@ -1020,42 +1058,60 @@ private int countOpenConnections() { * @since 1.7.9 */ protected ExecutorService getChannelsExecutor() { - if (getExecutorService() != null) { - return getExecutorService(); // NOSONAR never null - } - if (this.channelsExecutor == null) { - synchronized (this.connectionMonitor) { - if (this.channelsExecutor == null) { - final String threadPrefix = - getBeanName() == null - ? DEFAULT_DEFERRED_POOL_PREFIX + threadPoolId.incrementAndGet() - : getBeanName(); - ThreadFactory threadPoolFactory = new CustomizableThreadFactory(threadPrefix); // NOSONAR never null - this.channelsExecutor = Executors.newCachedThreadPool(threadPoolFactory); + ExecutorService executorService = getExecutorService(); + if (executorService == null) { + executorService = this.channelsExecutor; + if (executorService == null) { + this.connectionLock.lock(); + try { + executorService = this.channelsExecutor; + if (executorService == null) { + final String threadPrefix = + getBeanName() == null + ? DEFAULT_DEFERRED_POOL_PREFIX + threadPoolId.incrementAndGet() + : getBeanName(); + ThreadFactory threadPoolFactory = new CustomizableThreadFactory(threadPrefix); // NOSONAR never null + executorService = Executors.newCachedThreadPool(threadPoolFactory); + this.channelsExecutor = executorService; + } + } + finally { + this.connectionLock.unlock(); } } } - return this.channelsExecutor; + return executorService; } @Override public String toString() { - return "CachingConnectionFactory [channelCacheSize=" + this.channelCacheSize + ", host=" + getHost() - + ", port=" + getPort() + ", active=" + this.active + String host = getHost(); + int port = getPort(); + List

addresses = null; + try { + addresses = getAddresses(); + } + catch (IOException ex) { + host = "AddressResolver threw exception: " + ex.getMessage(); + } + return "CachingConnectionFactory [channelCacheSize=" + this.channelCacheSize + + (addresses != null + ? ", addresses=" + addresses + : (host != null ? ", host=" + host : "") + + (port > 0 ? ", port=" + port : "")) + + ", active=" + this.active + " " + super.toString() + "]"; } private final class CachedChannelInvocationHandler implements InvocationHandler { - private static final int ASYNC_CLOSE_TIMEOUT = 5_000; - private final ChannelCachingConnectionProxy theConnection; - private final LinkedList channelList; // NOSONAR addLast() + private final Deque channelList; private final String channelListIdentity; - private final Object targetMonitor = new Object(); + private final Lock targetLock = new ReentrantLock(); private final boolean transactional; @@ -1064,13 +1120,13 @@ private final class CachedChannelInvocationHandler implements InvocationHandler private final boolean publisherConfirms = ConfirmType.CORRELATED.equals(CachingConnectionFactory.this.confirmType); - private volatile Channel target; + private volatile @Nullable Channel target; private volatile boolean txStarted; CachedChannelInvocationHandler(ChannelCachingConnectionProxy connection, Channel target, - LinkedList channelList, // NOSONAR addLast() + Deque channelList, boolean transactional) { this.theConnection = connection; @@ -1081,12 +1137,12 @@ private final class CachedChannelInvocationHandler implements InvocationHandler } @Override // NOSONAR complexity - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // NOSONAR NCSS lines + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // NOSONAR NCSS lines + ChannelProxy channelProxy = (ChannelProxy) proxy; if (logger.isTraceEnabled() && !method.getName().equals("toString") && !method.getName().equals("hashCode") && !method.getName().equals("equals")) { try { - logger.trace(this.target + " channel." + method.getName() + "(" - + (args != null ? Arrays.toString(args) : "") + ")"); + logger.trace(this.target + " channel." + method.getName() + "(" + Arrays.toString(args) + ")"); } catch (Exception e) { // empty - some mocks fail here @@ -1096,46 +1152,52 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (methodName.equals("txSelect") && !this.transactional) { throw new UnsupportedOperationException("Cannot start transaction on non-transactional channel"); } - if (methodName.equals("equals")) { - // Only consider equal when proxies are identical. - return (proxy == args[0]); // NOSONAR - } - else if (methodName.equals("hashCode")) { - // Use hashCode of Channel proxy. - return System.identityHashCode(proxy); - } - else if (methodName.equals("toString")) { - return "Cached Rabbit Channel: " + this.target + ", conn: " + this.theConnection; - } - else if (methodName.equals("close")) { - // Handle close method: don't pass the call on. - if (CachingConnectionFactory.this.active && !RabbitUtils.isPhysicalCloseRequired()) { - logicalClose((ChannelProxy) proxy); - return null; + switch (methodName) { + case "equals" -> { + // Only consider equal when proxies are identical. + return (channelProxy == args[0]); // NOSONAR } - else { - physicalClose(proxy); + case "hashCode" -> { + // Use hashCode of Channel proxy. + return System.identityHashCode(channelProxy); + } + case "toString" -> { + return "Cached Rabbit Channel: " + this.target + ", conn: " + this.theConnection; + } + case "close" -> { + // Handle close method: don't pass the call on. + if (CachingConnectionFactory.this.active && !RabbitUtils.isPhysicalCloseRequired()) { + logicalClose(channelProxy); + } + else { + physicalClose(); + } return null; } - } - else if (methodName.equals("getTargetChannel")) { - // Handle getTargetChannel method: return underlying Channel. - return this.target; - } - else if (methodName.equals("isOpen")) { - // Handle isOpen method: we are closed if the target is closed - return this.target != null && this.target.isOpen(); - } - else if (methodName.equals("isTransactional")) { - return this.transactional; - } - else if (methodName.equals("isConfirmSelected")) { - return this.confirmSelected; + case "getTargetChannel" -> { + // Handle getTargetChannel method: return underlying Channel. + return this.target; + } + case "isOpen" -> { + // Handle isOpen method: we are closed if the target is closed + Channel targetToCheck = this.target; + return targetToCheck != null && targetToCheck.isOpen(); + } + case "isTransactional" -> { + return this.transactional; + } + case "isConfirmSelected" -> { + return this.confirmSelected; + } + case "isPublisherConfirms" -> { + return this.publisherConfirms; + } } try { - if (this.target == null || !this.target.isOpen()) { - if (this.target instanceof PublisherCallbackChannel) { - this.target.close(); + Channel targetChannel = this.target; + if (targetChannel == null || !targetChannel.isOpen()) { + if (targetChannel instanceof PublisherCallbackChannel) { + targetChannel.close(); throw new InvocationTargetException( new AmqpException("PublisherCallbackChannel is closed")); } @@ -1150,7 +1212,8 @@ else if (ackMethods.contains(methodName)) { } this.target = null; } - synchronized (this.targetMonitor) { + this.targetLock.lock(); + try { if (this.target == null) { this.target = createBareChannel(this.theConnection, this.transactional); } @@ -1165,42 +1228,42 @@ else if (txEnds.contains(methodName)) { } return result; } + finally { + this.targetLock.unlock(); + } } catch (InvocationTargetException ex) { - if (this.target == null || !this.target.isOpen()) { + Channel targetChannel = this.target; + if (targetChannel == null || !targetChannel.isOpen()) { // Basic re-connection logic... if (logger.isDebugEnabled()) { logger.debug("Detected closed channel on exception. Re-initializing: " + this.target); } this.target = null; - synchronized (this.targetMonitor) { + this.targetLock.lock(); + try { if (this.target == null) { this.target = createBareChannel(this.theConnection, this.transactional); } } + finally { + this.targetLock.unlock(); + } } throw ex.getTargetException(); } } - private void releasePermitIfNecessary(Object proxy) { + private void releasePermitIfNecessary() { if (CachingConnectionFactory.this.channelCheckoutTimeout > 0) { - /* - * Only release a permit if this is a normal close; if the channel is - * in the list, it means we're closing a cached channel (for which a permit - * has already been released). - */ - synchronized (this.channelList) { - if (this.channelList.contains(proxy)) { - return; - } - } Semaphore permits = CachingConnectionFactory.this.checkoutPermits.get(this.theConnection); if (permits != null) { - permits.release(); - if (logger.isDebugEnabled()) { - logger.debug("Released permit for '" + this.theConnection + "', remaining: " - + permits.availablePermits()); + if (permits.availablePermits() < CachingConnectionFactory.this.channelCacheSize) { + permits.release(); + if (logger.isDebugEnabled()) { + logger.debug("Released permit for '" + this.theConnection + "', remaining: " + + permits.availablePermits()); + } } } else { @@ -1219,43 +1282,73 @@ private void logicalClose(ChannelProxy proxy) throws IOException, TimeoutExcepti if (this.target == null) { return; } - if (this.target != null && !this.target.isOpen()) { - synchronized (this.targetMonitor) { - if (this.target != null && !this.target.isOpen()) { - if (this.target instanceof PublisherCallbackChannel) { - this.target.close(); // emit nacks if necessary - } - if (this.channelList.contains(proxy)) { - this.channelList.remove(proxy); + Channel targetChannel = this.target; + if (targetChannel != null && !targetChannel.isOpen()) { + this.targetLock.lock(); + try { + targetChannel = this.target; + if (targetChannel != null && !targetChannel.isOpen()) { + if (targetChannel instanceof PublisherCallbackChannel) { + targetChannel.close(); // emit nacks if necessary } - else { - releasePermitIfNecessary(proxy); + if (!this.channelList.remove(proxy)) { + releasePermitIfNecessary(); } this.target = null; return; } } + finally { + this.targetLock.unlock(); + } } returnToCache(proxy); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void returnToCache(ChannelProxy proxy) { - if (CachingConnectionFactory.this.active && this.publisherConfirms - && proxy instanceof PublisherCallbackChannel) { + if (CachingConnectionFactory.this.active + && this.publisherConfirms + && proxy instanceof PublisherCallbackChannel publisherCallbackChannel) { this.theConnection.channelsAwaitingAcks.put(this.target, proxy); - ((PublisherCallbackChannel) proxy) - .setAfterAckCallback(c -> - doReturnToCache(this.theConnection.channelsAwaitingAcks.remove(c))); + AtomicBoolean ackCallbackCalledImmediately = new AtomicBoolean(); + publisherCallbackChannel + .setAfterAckCallback(c -> { + ackCallbackCalledImmediately.set(true); + doReturnToCache(this.theConnection.channelsAwaitingAcks.remove(c)); + }); + + if (!ackCallbackCalledImmediately.get()) { + getChannelsExecutor() + .execute(() -> { + try { + publisherCallbackChannel.waitForConfirms(getCloseTimeout()); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + catch (ShutdownSignalException | TimeoutException ex) { + // The channel didn't handle confirms, so close it altogether to avoid + // memory leaks for pending confirms + try { + physicalClose(); + } + catch (@SuppressWarnings(UNUSED) Exception e) { + } + } + }); + } } else { doReturnToCache(proxy); } } - private void doReturnToCache(Channel proxy) { + private void doReturnToCache(@Nullable ChannelProxy proxy) { if (proxy != null) { - synchronized (this.channelList) { + this.theConnection.channelListLock.lock(); + try { // Allow for multiple close calls... if (CachingConnectionFactory.this.active) { cacheOrClose(proxy); @@ -1263,24 +1356,27 @@ private void doReturnToCache(Channel proxy) { else { if (proxy.isOpen()) { try { - physicalClose(proxy); + physicalClose(); } catch (@SuppressWarnings(UNUSED) Exception e) { } } } } + finally { + this.theConnection.channelListLock.unlock(); + } } } - private void cacheOrClose(Channel proxy) { + private void cacheOrClose(ChannelProxy proxy) { boolean alreadyCached = this.channelList.contains(proxy); if (this.channelList.size() >= getChannelCacheSize() && !alreadyCached) { if (logger.isTraceEnabled()) { logger.trace("Cache limit reached: " + this.target); } try { - physicalClose(proxy); + physicalClose(); } catch (@SuppressWarnings(UNUSED) Exception e) { } @@ -1289,8 +1385,8 @@ else if (!alreadyCached) { if (logger.isTraceEnabled()) { logger.trace("Returning cached Channel: " + this.target); } - releasePermitIfNecessary(proxy); - this.channelList.addLast((ChannelProxy) proxy); + this.channelList.addLast(proxy); + releasePermitIfNecessary(); setHighWaterMark(); } } @@ -1307,7 +1403,8 @@ private void setHighWaterMark() { } } - private void physicalClose(Object proxy) throws IOException, TimeoutException { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private void physicalClose() throws IOException, TimeoutException { if (logger.isDebugEnabled()) { logger.debug("Closing cached Channel: " + this.target); } @@ -1321,12 +1418,12 @@ private void physicalClose(Object proxy) throws IOException, TimeoutException { (ConfirmType.CORRELATED.equals(CachingConnectionFactory.this.confirmType) || CachingConnectionFactory.this.publisherReturns)) { async = true; - asyncClose(proxy); + asyncClose(); } else { this.target.close(); - if (this.target instanceof AutorecoveringChannel) { - ClosingRecoveryListener.removeChannel((AutorecoveringChannel) this.target); + if (this.target instanceof AutorecoveringChannel auto) { + ClosingRecoveryListener.removeChannel(auto); } } } @@ -1338,12 +1435,13 @@ private void physicalClose(Object proxy) throws IOException, TimeoutException { finally { this.target = null; if (!async) { - releasePermitIfNecessary(proxy); + releasePermitIfNecessary(); } } } - private void asyncClose(Object proxy) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private void asyncClose() { ExecutorService executorService = getChannelsExecutor(); final Channel channel = CachedChannelInvocationHandler.this.target; CachingConnectionFactory.this.inFlightAsyncCloses.add(channel); @@ -1351,10 +1449,10 @@ private void asyncClose(Object proxy) { executorService.execute(() -> { try { if (ConfirmType.CORRELATED.equals(CachingConnectionFactory.this.confirmType)) { - channel.waitForConfirmsOrDie(ASYNC_CLOSE_TIMEOUT); + channel.waitForConfirmsOrDie(getCloseTimeout()); } else { - Thread.sleep(ASYNC_CLOSE_TIMEOUT); + Thread.sleep(5_000); // NOSONAR - some time to give the channel a chance to ack } } catch (@SuppressWarnings(UNUSED) InterruptedException e1) { @@ -1366,11 +1464,7 @@ private void asyncClose(Object proxy) { try { channel.close(); } - catch (@SuppressWarnings(UNUSED) IOException e3) { - } - catch (@SuppressWarnings(UNUSED) AlreadyClosedException e4) { - } - catch (@SuppressWarnings(UNUSED) TimeoutException e5) { + catch (@SuppressWarnings(UNUSED) IOException | AlreadyClosedException | TimeoutException e3) { } catch (ShutdownSignalException e6) { if (!RabbitUtils.isNormalShutdown(e6)) { @@ -1379,7 +1473,7 @@ private void asyncClose(Object proxy) { } finally { CachingConnectionFactory.this.inFlightAsyncCloses.release(channel); - releasePermitIfNecessary(proxy); + releasePermitIfNecessary(); } } }); @@ -1395,9 +1489,11 @@ private class ChannelCachingConnectionProxy implements ConnectionProxy { // NOSO private final AtomicBoolean closeNotified = new AtomicBoolean(false); + private final Lock channelListLock = new ReentrantLock(); + private final ConcurrentMap channelsAwaitingAcks = new ConcurrentHashMap<>(); - private volatile Connection target; + private volatile @Nullable Connection target; ChannelCachingConnectionProxy(@Nullable Connection target) { this.target = target; @@ -1428,7 +1524,8 @@ public boolean removeBlockedListener(BlockedListener listener) { @Override public void close() { if (CachingConnectionFactory.this.cacheMode == CacheMode.CONNECTION) { - synchronized (CachingConnectionFactory.this.connectionMonitor) { + CachingConnectionFactory.this.connectionLock.lock(); + try { /* * Only connectionCacheSize open idle connections are allowed. */ @@ -1449,9 +1546,12 @@ public void close() { CachingConnectionFactory.this.connectionHighWaterMark .set(CachingConnectionFactory.this.idleConnections.size()); } - CachingConnectionFactory.this.connectionMonitor.notifyAll(); + CachingConnectionFactory.this.connectionAvailableCondition.signalAll(); } } + finally { + CachingConnectionFactory.this.connectionLock.unlock(); + } } } @@ -1465,6 +1565,7 @@ private int countOpenIdleConnections() { return n; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void destroy() { if (CachingConnectionFactory.this.cacheMode == CacheMode.CHANNEL) { reset(CachingConnectionFactory.this.cachedChannelsNonTransactional, @@ -1490,17 +1591,24 @@ private void notifyCloseIfNecessary() { @Override public boolean isOpen() { - return this.target != null && this.target.isOpen(); + Connection targetToCheck = this.target; + return targetToCheck != null && targetToCheck.isOpen(); } @Override - public Connection getTargetConnection() { + public @Nullable Connection getTargetConnection() { return this.target; } @Override public com.rabbitmq.client.Connection getDelegate() { - return this.target.getDelegate(); + Connection targetConnection = this.target; + if (targetConnection != null) { + return targetConnection.getDelegate(); + } + else { + throw new IllegalStateException("Can't get delegate - no target connection."); + } } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelListener.java index ad9f43e5ea..8d3451b620 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ public interface ChannelListener { /** * Called when the underlying RabbitMQ channel is closed for any * reason. - * @param signal the shut down signal. + * @param signal the shutdown signal. */ default void onShutDown(ShutdownSignalException signal) { } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java index 8e394b8eea..b2d382eaf6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,18 @@ import com.rabbitmq.client.Channel; +import org.springframework.aop.RawTargetAccess; + /** * Subinterface of {@link com.rabbitmq.client.Channel} to be implemented by * Channel proxies. Allows access to the underlying target Channel * * @author Mark Pollack * @author Gary Russell + * @author Leonardo Ferreira * @see CachingConnectionFactory */ -public interface ChannelProxy extends Channel { +public interface ChannelProxy extends Channel, RawTargetAccess { /** * Return the target Channel of this proxy. @@ -44,11 +47,19 @@ public interface ChannelProxy extends Channel { /** * Return true if confirms are selected on this channel. - * @return true if confirms selected. + * @return true if {@code confirms} selected. * @since 2.1 */ default boolean isConfirmSelected() { return false; } + /** + * Return true if publisher confirms are enabled. + * @return true if publisherConfirms. + */ + default boolean isPublisherConfirms() { + return false; + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java index 1f66d5605c..b12467bdfd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +21,12 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeoutException; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import com.rabbitmq.client.Channel; import com.rabbitmq.client.Recoverable; import com.rabbitmq.client.RecoveryListener; import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; /** * A {@link RecoveryListener} that closes the recovered channel, to avoid @@ -79,14 +78,13 @@ public void handleRecoveryStarted(Recoverable recoverable) { */ public static void addRecoveryListenerIfNecessary(Channel channel) { AutorecoveringChannel autorecoveringChannel = null; - if (channel instanceof ChannelProxy) { - if (((ChannelProxy) channel).getTargetChannel() instanceof AutorecoveringChannel) { - autorecoveringChannel = (AutorecoveringChannel) ((ChannelProxy) channel) - .getTargetChannel(); + if (channel instanceof ChannelProxy proxy) { + if (proxy.getTargetChannel() instanceof AutorecoveringChannel auto) { + autorecoveringChannel = auto; } } - else if (channel instanceof AutorecoveringChannel) { - autorecoveringChannel = (AutorecoveringChannel) channel; + else if (channel instanceof AutorecoveringChannel auto) { + autorecoveringChannel = auto; } if (autorecoveringChannel != null && hasListener.putIfAbsent(autorecoveringChannel, Boolean.TRUE) == null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeChannelListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeChannelListener.java index 81c7dd532f..66399f31e4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeChannelListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeChannelListener.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,11 +25,12 @@ /** * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * */ public class CompositeChannelListener implements ChannelListener { - private List delegates = new ArrayList(); + private List delegates = new ArrayList<>(); public void onCreate(Channel channel, boolean transactional) { for (ChannelListener delegate : this.delegates) { @@ -45,7 +46,7 @@ public void onShutDown(ShutdownSignalException signal) { } public void setDelegates(List delegates) { - this.delegates = new ArrayList(delegates); + this.delegates = new ArrayList<>(delegates); } public void addDelegate(ChannelListener delegate) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java index ce1b01a7b2..496205a976 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,20 +21,22 @@ import java.util.concurrent.CopyOnWriteArrayList; import com.rabbitmq.client.ShutdownSignalException; +import org.jspecify.annotations.Nullable; /** - * A composite listener that invokes its delegages in turn. + * A composite listener that invokes its delegates in turn. * * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * */ public class CompositeConnectionListener implements ConnectionListener { - private List delegates = new CopyOnWriteArrayList(); + private List delegates = new CopyOnWriteArrayList<>(); @Override - public void onCreate(Connection connection) { + public void onCreate(@Nullable Connection connection) { this.delegates.forEach(delegate -> delegate.onCreate(connection)); } @@ -54,7 +56,7 @@ public void onFailed(Exception exception) { } public void setDelegates(List delegates) { - this.delegates = new ArrayList(delegates); + this.delegates = new ArrayList<>(delegates); } public void addDelegate(ConnectionListener delegate) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java index 04ff8c38ec..9428fb0422 100755 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,11 @@ package org.springframework.amqp.rabbit.connection; -import org.springframework.amqp.AmqpException; -import org.springframework.lang.Nullable; - import com.rabbitmq.client.BlockedListener; import com.rabbitmq.client.Channel; +import org.springframework.amqp.AmqpException; + /** * @author Dave Syer * @author Gary Russell @@ -41,9 +40,8 @@ public interface Connection extends AutoCloseable { * Close this connection and all its channels * with the {@link com.rabbitmq.client.AMQP#REPLY_SUCCESS} close code * and message 'OK'. - * + *

* Waits for all the close operations to complete. - * * @throws AmqpException if an I/O problem is encountered */ @Override @@ -61,7 +59,6 @@ public interface Connection extends AutoCloseable { */ int getLocalPort(); - /** * Add a {@link BlockedListener}. * @param listener the listener to add @@ -84,9 +81,7 @@ public interface Connection extends AutoCloseable { * Return the underlying RabbitMQ connection. * @return the connection. */ - default @Nullable com.rabbitmq.client.Connection getDelegate() { - return null; - } + com.rabbitmq.client.Connection getDelegate(); /** * Close any channel associated with the current thread. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java index 4b1e049be6..aaa0d0eb6b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.amqp.rabbit.connection; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; -import org.springframework.lang.Nullable; /** * An interface based ConnectionFactory for creating {@link com.rabbitmq.client.Connection Connections}. @@ -33,6 +34,7 @@ public interface ConnectionFactory { Connection createConnection() throws AmqpException; + @Nullable String getHost(); int getPort(); @@ -83,4 +85,12 @@ default boolean isPublisherReturns() { return false; } + /** + * Close any connection(s) that might be cached by this factory. This does not prevent + * new connections from being opened. + * @since 2.4.4 + */ + default void resetConnection() { + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryConfigurationUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryConfigurationUtils.java index 32968e629b..e6e31f3e3e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryConfigurationUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryConfigurationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Utility methods for configuring connection factories. * @@ -38,7 +40,7 @@ private ConnectionFactoryConfigurationUtils() { * @param clientConnectionProperties the properties. */ public static void updateClientConnectionProperties(AbstractConnectionFactory connectionFactory, - String clientConnectionProperties) { + @Nullable String clientConnectionProperties) { if (clientConnectionProperties != null) { String[] props = clientConnectionProperties.split(","); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapper.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapper.java index 57b5c2daad..7295c3165c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapper.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,8 @@ import java.util.concurrent.Callable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java index 8316518f6b..d5aa3950aa 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,15 @@ import java.io.IOException; import java.util.function.Consumer; +import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpIOException; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSynchronization; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; - -import com.rabbitmq.client.Channel; +import org.springframework.util.ClassUtils; /** * Helper class for managing a Spring based Rabbit {@link org.springframework.amqp.rabbit.connection.ConnectionFactory}, @@ -43,7 +44,11 @@ */ public final class ConnectionFactoryUtils { - private static final ThreadLocal COMPLETION_EXCEPTIONS = new ThreadLocal<>(); + private static final boolean WEB_FLUX_PRESENT = + ClassUtils.isPresent("org.springframework.web.reactive.function.client.WebClient", + ConnectionFactoryUtils.class.getClassLoader()); + + private static final ThreadLocal<@Nullable AfterCompletionFailedException> COMPLETION_EXCEPTIONS = new ThreadLocal<>(); private static boolean captureAfterCompletionExceptions; @@ -58,9 +63,6 @@ private ConnectionFactoryUtils() { * @return whether the Channel is transactional */ public static boolean isChannelTransactional(Channel channel, ConnectionFactory connectionFactory) { - if (channel == null || connectionFactory == null) { - return false; - } RabbitResourceHolder resourceHolder = (RabbitResourceHolder) TransactionSynchronizationManager .getResource(connectionFactory); return (resourceHolder != null && resourceHolder.containsChannel(channel)); @@ -77,6 +79,7 @@ public static boolean isChannelTransactional(Channel channel, ConnectionFactory */ public static RabbitResourceHolder getTransactionalResourceHolder(final ConnectionFactory connectionFactory, final boolean synchedLocalTransactionAllowed) { + return getTransactionalResourceHolder(connectionFactory, synchedLocalTransactionAllowed, false); } @@ -103,8 +106,9 @@ public static RabbitResourceHolder getTransactionalResourceHolder(final Connecti * @param connectionFactory the RabbitMQ ConnectionFactory to bind for (used as TransactionSynchronizationManager * key) * @param resourceFactory the ResourceFactory to use for extracting or creating RabbitMQ resources - * @return the transactional Channel, or null if none found + * @return the transactional Channel */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation private static RabbitResourceHolder doGetTransactionalResourceHolder(// NOSONAR complexity ConnectionFactory connectionFactory, ResourceFactory resourceFactory) { @@ -124,7 +128,7 @@ private static RabbitResourceHolder doGetTransactionalResourceHolder(// NOSONAR resourceHolderToUse = new RabbitResourceHolder(); } Connection connection = resourceFactory.getConnection(resourceHolderToUse); //NOSONAR - Channel channel = null; + Channel channel; try { /* * If we are in a listener container, first see if there's a channel registered @@ -155,7 +159,7 @@ private static RabbitResourceHolder doGetTransactionalResourceHolder(// NOSONAR if (!resourceHolderToUse.equals(resourceHolder) && TransactionSynchronizationManager.isSynchronizationActive()) { bindResourceToTransaction(resourceHolderToUse, connectionFactory, - resourceFactory.isSynchedLocalTransactionAllowed()); + resourceFactory.synchedLocalTransactionAllowed()); } return resourceHolderToUse; @@ -175,7 +179,7 @@ public static void releaseResources(@Nullable RabbitResourceHolder resourceHolde RabbitUtils.closeConnection(resourceHolder.getConnection()); } - public static RabbitResourceHolder bindResourceToTransaction(RabbitResourceHolder resourceHolder, + public static @Nullable RabbitResourceHolder bindResourceToTransaction(RabbitResourceHolder resourceHolder, ConnectionFactory connectionFactory, boolean synched) { if (TransactionSynchronizationManager.hasResource(connectionFactory) @@ -252,6 +256,15 @@ public static Connection createConnection(final ConnectionFactory connectionFact return connectionFactory.createConnection(); } + static NodeLocator nodeLocator() { + if (WEB_FLUX_PRESENT) { + return new WebFluxNodeLocator(); + } + else { + return new RestTemplateNodeLocator(); + } + } + /** * Callback interface for resource creation. Serving as argument for the doGetTransactionalChannel * method. @@ -271,6 +284,7 @@ public interface ResourceFactory { * @param holder the RabbitResourceHolder * @return an appropriate Connection fetched from the holder, or null if none found */ + @Nullable Connection getConnection(RabbitResourceHolder holder); /** @@ -294,35 +308,20 @@ public interface ResourceFactory { * with the RabbitMQ transaction committing right after the main transaction. * @return whether to allow for synchronizing a local RabbitMQ transaction */ - boolean isSynchedLocalTransactionAllowed(); + boolean synchedLocalTransactionAllowed(); } - private static class RabbitResourceFactory implements ResourceFactory { - - private final ConnectionFactory connectionFactory; - - private final boolean synchedLocalTransactionAllowed; - - private final boolean publisherConnectionIfPossible; - - RabbitResourceFactory(ConnectionFactory connectionFactory, boolean synchedLocalTransactionAllowed, - boolean publisherConnectionIfPossible) { - - this.connectionFactory = connectionFactory; - this.synchedLocalTransactionAllowed = synchedLocalTransactionAllowed; - this.publisherConnectionIfPossible = publisherConnectionIfPossible; - } + private record RabbitResourceFactory(ConnectionFactory connectionFactory, boolean synchedLocalTransactionAllowed, + boolean publisherConnectionIfPossible) implements ResourceFactory { @Override - @Nullable - public Channel getChannel(RabbitResourceHolder holder) { + public @Nullable Channel getChannel(RabbitResourceHolder holder) { return holder.getChannel(); } @Override - @Nullable - public Connection getConnection(RabbitResourceHolder holder) { + public @Nullable Connection getConnection(RabbitResourceHolder holder) { return holder.getConnection(); } @@ -337,11 +336,6 @@ public Channel createChannel(Connection con) { return con.createChannel(this.synchedLocalTransactionAllowed); } - @Override - public boolean isSynchedLocalTransactionAllowed() { - return this.synchedLocalTransactionAllowed; - } - } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionListener.java index 7154bbf99b..61ec8df30c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.connection; import com.rabbitmq.client.ShutdownSignalException; +import org.jspecify.annotations.Nullable; /** * A listener for connection creation and closing. @@ -32,7 +33,7 @@ public interface ConnectionListener { * Called when a new connection is established. * @param connection the connection. */ - void onCreate(Connection connection); + void onCreate(@Nullable Connection connection); /** * Called when a connection is closed. @@ -44,7 +45,7 @@ default void onClose(Connection connection) { /** * Called when a connection is force closed. - * @param signal the shut down signal. + * @param signal the shutdown signal. * @since 2.0 */ default void onShutDown(ShutdownSignalException signal) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionProxy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionProxy.java index 29d4a2be82..b573fb9c57 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionProxy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.connection; +import org.jspecify.annotations.Nullable; + /** * Subinterface of {@link Connection} to be implemented by * Connection proxies. Allows access to the underlying target Connection @@ -28,7 +30,9 @@ public interface ConnectionProxy extends Connection { /** * Return the target Channel of this proxy. *

This will typically be the native provider Connection - * @return the underlying Connection (never null) + * @return the underlying Connection (if any) */ + @Nullable Connection getTargetConnection(); + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java index 1d2b61abef..51ee5febbd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,10 @@ package org.springframework.amqp.rabbit.connection; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; - -import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; /** * Consumers register their primary channels with this class. This is used @@ -31,6 +29,7 @@ * tangle with RabbitResourceHolder. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.2 * */ @@ -38,8 +37,7 @@ public final class ConsumerChannelRegistry { private static final Log logger = LogFactory.getLog(ConsumerChannelRegistry.class); // NOSONAR - lower case - private static final ThreadLocal consumerChannel // NOSONAR - lower case - = new ThreadLocal(); + private static final ThreadLocal<@Nullable ChannelHolder> CONSUMER_CHANNEL = new ThreadLocal<>(); private ConsumerChannelRegistry() { } @@ -58,9 +56,9 @@ private ConsumerChannelRegistry() { public static void registerConsumerChannel(Channel channel, ConnectionFactory connectionFactory) { if (logger.isDebugEnabled()) { logger.debug("Registering consumer channel" + channel + " from factory " + - connectionFactory); + connectionFactory); } - consumerChannel.set(new ChannelHolder(channel, connectionFactory)); + CONSUMER_CHANNEL.set(new ChannelHolder(channel, connectionFactory)); } /** @@ -69,9 +67,9 @@ public static void registerConsumerChannel(Channel channel, ConnectionFactory co */ public static void unRegisterConsumerChannel() { if (logger.isDebugEnabled()) { - logger.debug("Unregistering consumer channel" + consumerChannel.get()); + logger.debug("Unregistering consumer channel" + CONSUMER_CHANNEL.get()); } - consumerChannel.remove(); + CONSUMER_CHANNEL.remove(); } /** @@ -80,14 +78,11 @@ public static void unRegisterConsumerChannel() { * * @return The channel. */ - @Nullable - public static Channel getConsumerChannel() { - ChannelHolder channelHolder = consumerChannel.get(); - Channel channel = null; - if (channelHolder != null) { - channel = channelHolder.getChannel(); - } - return channel; + public static @Nullable Channel getConsumerChannel() { + ChannelHolder channelHolder = CONSUMER_CHANNEL.get(); + return channelHolder != null + ? channelHolder.channel() + : null; } /** @@ -97,33 +92,17 @@ public static Channel getConsumerChannel() { * @param connectionFactory The connection factory. * @return The channel. */ - @Nullable - public static Channel getConsumerChannel(ConnectionFactory connectionFactory) { - ChannelHolder channelHolder = consumerChannel.get(); + public static @Nullable Channel getConsumerChannel(ConnectionFactory connectionFactory) { + ChannelHolder channelHolder = CONSUMER_CHANNEL.get(); Channel channel = null; - if (channelHolder != null && channelHolder.getConnectionFactory() == connectionFactory) { - channel = channelHolder.getChannel(); + if (channelHolder != null && channelHolder.connectionFactory().equals(connectionFactory)) { + channel = channelHolder.channel(); } return channel; } - private static final class ChannelHolder { - - private final Channel channel; + private record ChannelHolder(Channel channel, ConnectionFactory connectionFactory) { - private final ConnectionFactory connectionFactory; - - ChannelHolder(Channel channel, ConnectionFactory connectionFactory) { - this.channel = channel; - this.connectionFactory = connectionFactory; - } - - private Channel getChannel() { - return this.channel; - } - - private ConnectionFactory getConnectionFactory() { - return this.connectionFactory; - } } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java index d921a24ae9..a9129dfe0d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,13 @@ package org.springframework.amqp.rabbit.connection; import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Correlation; -import org.springframework.amqp.core.Message; import org.springframework.amqp.core.ReturnedMessage; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.concurrent.SettableListenableFuture; /** * Base class for correlating publisher confirms to sent messages. Use the @@ -41,14 +41,14 @@ */ public class CorrelationData implements Correlation { - private final SettableListenableFuture future = new SettableListenableFuture<>(); + private final CompletableFuture future = new CompletableFuture<>(); private volatile String id; - private volatile ReturnedMessage returnedMessage; + private volatile @Nullable ReturnedMessage returnedMessage; /** - * Construct an instance with a null Id. + * Construct an instance with a null {@code Id}. * @since 1.6.7 */ public CorrelationData() { @@ -77,7 +77,6 @@ public String getId() { * Set the correlation id. Generally, the correlation id shouldn't be changed. * One use case, however, is when it needs to be set in a * {@link org.springframework.amqp.core.MessagePostProcessor}. - * * @param id the id. * @since 1.6 */ @@ -91,49 +90,17 @@ public void setId(String id) { * @return the future. * @since 2.1 */ - public SettableListenableFuture getFuture() { + public CompletableFuture getFuture() { return this.future; } - /** - * Return a returned message, if any; requires a unique - * {@link #CorrelationData(String) id}. Guaranteed to be populated before the future - * is set. - * @deprecated in favor of {@link #getReturned()}. - * @return the message or null. - * @since 2.1 - */ - @Deprecated - @Nullable - public Message getReturnedMessage() { - if (this.returnedMessage == null) { - return null; - } - else { - return this.returnedMessage.getMessage(); - } - } - - /** - * Set a returned message for this correlation data. - * @param returnedMessage the returned message. - * @deprecated in favor of {@link #setReturned(ReturnedMessage)}. - * @since 1.7.13 - */ - @Deprecated - public void setReturnedMessage(Message returnedMessage) { - this.returnedMessage = new ReturnedMessage(returnedMessage, 0, "not available", "not available", - "not available"); - } - /** * Get the returned message and metadata, if any. Guaranteed to be populated before - * the future is set. + * the future is completed. * @return the {@link ReturnedMessage}. * @since 2.3.3 */ - @Nullable - public ReturnedMessage getReturned() { + public @Nullable ReturnedMessage getReturned() { return this.returnedMessage; } @@ -154,36 +121,25 @@ public String toString() { /** * Represents a publisher confirmation. When the ack field is * true, the publish was successful; otherwise failed with a possible - * reason (may be null, meaning unknown). + * reason (maybe null, meaning unknown). + * + * @param ack true to confirm + * @param reason the reason for nack * * @since 2.1 */ - public static class Confirm { - - private final boolean ack; - - private final String reason; - - public Confirm(boolean ack, @Nullable String reason) { - this.ack = ack; - this.reason = reason; - } + public record Confirm(boolean ack, @Nullable String reason) { + @Deprecated(forRemoval = true, since = "4.0") public boolean isAck() { return this.ack; } - public String getReason() { + @Deprecated(forRemoval = true, since = "4.0") + public @Nullable String getReason() { return this.reason; } - @Override - public String toString() { - return "Confirm [ack=" + this.ack - + (this.reason != null ? ", reason=" + this.reason : "") - + "]"; - } - } } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceInterface.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java similarity index 51% rename from spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceInterface.java rename to spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java index a5b78a3eef..0d75ab329b 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceInterface.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2022-2025 the original author 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,27 +14,27 @@ * limitations under the License. */ -package org.springframework.amqp.remoting.testservice; +package org.springframework.amqp.rabbit.connection; + +import org.jspecify.annotations.Nullable; /** - * @author David Bilge + * Callback to determine the connection factory using the provided information. + * * @author Gary Russell - * @since 1.2 + * @since 2.4.8 */ -public interface TestServiceInterface { - - void simpleTestMethod(); - - String simpleStringReturningTestMethod(String string); - - void exceptionThrowingMethod(); - - Object echo(Object o); - - SpecialException notReallyExceptionReturningMethod(); - - SpecialException actuallyExceptionReturningMethod(); - - Object simulatedTimeoutMethod(Object o); +@FunctionalInterface +public interface FactoryFinder { + + /** + * Locate or create a factory. + * @param queueName the queue name. + * @param node the node name. + * @param nodeUri the node URI. + * @return the factory. + */ + @Nullable + ConnectionFactory locate(@Nullable String queueName, String node, String nodeUri); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index fc04a7319d..a4efd22a2f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or 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,28 @@ package org.springframework.amqp.rabbit.connection; -import java.net.MalformedURLException; -import java.net.URISyntaxException; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.SmartLifecycle; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; - /** * A {@link RoutingConnectionFactory} that determines the node on which a queue is located and * returns a factory that connects directly to that node. @@ -52,19 +52,24 @@ *

All {@link ConnectionFactory} methods delegate to the default * * @author Gary Russell + * @author Christian Tzolov + * @author Ngoc Nhan * @since 1.2 */ -public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean { +public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean, + SmartLifecycle { private final Log logger = LogFactory.getLog(getClass()); - private final Map nodeFactories = new HashMap(); + private final Lock lock = new ReentrantLock(); + + private final Map nodeFactories = new HashMap<>(); private final ConnectionFactory defaultConnectionFactory; private final String[] adminUris; - private final Map nodeToAddress = new HashMap(); + private final Map nodeToAddress = new HashMap<>(); private final String vhost; @@ -74,24 +79,28 @@ public class LocalizedQueueConnectionFactory implements ConnectionFactory, Routi private final boolean useSSL; - private final Resource sslPropertiesLocation; + private final @Nullable Resource sslPropertiesLocation; + + private final @Nullable String keyStore; - private final String keyStore; + private final @Nullable String trustStore; - private final String trustStore; + private final @Nullable String keyStorePassPhrase; - private final String keyStorePassPhrase; + private final @Nullable String trustStorePassPhrase; - private final String trustStorePassPhrase; + private final AtomicBoolean running = new AtomicBoolean(); + + private NodeLocator nodeLocator; /** * @param defaultConnectionFactory the fallback connection factory to use if the queue * can't be located. - * @param nodeToAddress a Map of node to address: (rabbit@server1 : server1:5672) - * @param adminUris the rabbitmq admin addresses (https://host:port, ...) must be the + * @param nodeToAddress a Map of node to address: {@code rabbit@server1 : server1:5672} + * @param adminUris the rabbitmq admin addresses ({@code https://host:port, ...}) must be the * same length as addresses. * @param vhost the virtual host. - * @param username the user name. + * @param username the username. * @param password the password. * @param useSSL use SSL. * @param sslPropertiesLocation the SSL properties location. @@ -106,11 +115,11 @@ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactor /** * @param defaultConnectionFactory the fallback connection factory to use if the queue can't be located. - * @param nodeToAddress a Map of node to address: (rabbit@server1 : server1:5672) - * @param adminUris the rabbitmq admin addresses (https://host:port, ...) must be the same length + * @param nodeToAddress a Map of node to address: {@code rabbit@server1 : server1:5672} + * @param adminUris the rabbitmq admin addresses ({@code https://host:port, ...}) must be the same length * as addresses. * @param vhost the virtual host. - * @param username the user name. + * @param username the username. * @param password the password. * @param useSSL use SSL. * @param keyStore the key store resource (e.g. "file:/foo/keystore"). @@ -131,11 +140,11 @@ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactor * @param defaultConnectionFactory the fallback connection factory to use if the queue * can't be located. * @param addresses the rabbitmq server addresses (host:port, ...). - * @param adminUris the rabbitmq admin addresses (https://host:port, ...) + * @param adminUris the rabbitmq admin addresses ({@code https://host:port, ...}) * @param nodes the rabbitmq nodes corresponding to addresses (rabbit@server1, ...) * must be the same length as addresses. * @param vhost the virtual host. - * @param username the user name. + * @param username the username. * @param password the password. * @param useSSL use SSL. * @param sslPropertiesLocation the SSL properties location. @@ -151,11 +160,11 @@ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactor /** * @param defaultConnectionFactory the fallback connection factory to use if the queue can't be located. * @param addresses the rabbitmq server addresses (host:port, ...). - * @param adminUris the rabbitmq admin addresses (https://host:port, ...). + * @param adminUris the rabbitmq admin addresses ({@code https://host:port, ...}). * @param nodes the rabbitmq nodes corresponding to addresses (rabbit@server1, ...) must be the same length * as addresses. * @param vhost the virtual host. - * @param username the user name. + * @param username the username. * @param password the password. * @param useSSL use SSL. * @param keyStore the key store resource (e.g. "file:/foo/keystore"). @@ -190,14 +199,25 @@ private LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFacto this.trustStore = trustStore; this.keyStorePassPhrase = keyStorePassPhrase; this.trustStorePassPhrase = trustStorePassPhrase; + this.nodeLocator = ConnectionFactoryUtils.nodeLocator(); } private static Map nodesAddressesToMap(String[] nodes, String[] addresses) { Assert.isTrue(addresses.length == nodes.length, "'addresses' and 'nodes' properties must have equal length"); return IntStream.range(0, addresses.length) - .mapToObj(i -> new SimpleImmutableEntry<>(nodes[i], addresses[i])) - .collect(Collectors.toMap(SimpleImmutableEntry::getKey, SimpleImmutableEntry::getValue)); + .mapToObj(i -> new SimpleImmutableEntry<>(nodes[i], addresses[i])) + .collect(Collectors.toMap(SimpleImmutableEntry::getKey, SimpleImmutableEntry::getValue)); + } + + /** + * Set a {@link NodeLocator} to use to find the node address for the leader. + * @param nodeLocator the locator. + * @since 2.4.8 + */ + public void setNodeLocator(NodeLocator nodeLocator) { + Assert.notNull(nodeLocator, "'nodeLocator' cannot be null"); + this.nodeLocator = nodeLocator; } @Override @@ -206,7 +226,7 @@ public Connection createConnection() throws AmqpException { } @Override - public String getHost() { + public @Nullable String getHost() { return this.defaultConnectionFactory.getHost(); } @@ -225,6 +245,27 @@ public String getUsername() { return this.username; } + @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + + @Override + public void start() { + this.running.set(true); + } + + @Override + public void stop() { + this.running.set(false); + resetConnection(); + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + @Override public void addConnectionListener(ConnectionListener listener) { this.defaultConnectionFactory.addConnectionListener(listener); @@ -244,78 +285,40 @@ public void clearConnectionListeners() { public ConnectionFactory getTargetConnectionFactory(Object key) { String queue = ((String) key); queue = queue.substring(1, queue.length() - 1); - Assert.isTrue(!queue.contains(","), () -> "Cannot use LocalizedQueueConnectionFactory with more than one queue: " + key); + Assert.isTrue(!queue.contains(","), + () -> "Cannot use LocalizedQueueConnectionFactory with more than one queue: " + key); ConnectionFactory connectionFactory = determineConnectionFactory(queue); - if (connectionFactory == null) { - return this.defaultConnectionFactory; - } - else { - return connectionFactory; - } + return Objects.requireNonNullElse(connectionFactory, this.defaultConnectionFactory); } - @Nullable - private ConnectionFactory determineConnectionFactory(String queue) { - for (int i = 0; i < this.adminUris.length; i++) { - String adminUri = this.adminUris[i]; - if (!adminUri.endsWith("/api/")) { - adminUri += "/api/"; - } - try { - Client client = createClient(adminUri, this.username, this.password); - QueueInfo queueInfo = client.getQueue(this.vhost, queue); - if (queueInfo != null) { - String node = queueInfo.getNode(); - if (node != null) { - String uri = this.nodeToAddress.get(node); - if (uri != null) { - return nodeConnectionFactory(queue, node, uri); - } - if (this.logger.isDebugEnabled()) { - this.logger.debug("No match for node: " + node); - } - } - } - else { - throw new AmqpException("Admin returned null QueueInfo"); - } - } - catch (Exception e) { - this.logger.warn("Failed to determine queue location for: " + queue + " at: " + - adminUri + ": " + e.getMessage()); - } + private @Nullable ConnectionFactory determineConnectionFactory(String queue) { + ConnectionFactory cf = this.nodeLocator.locate(this.adminUris, this.nodeToAddress, this.vhost, this.username, + this.password, queue, this::nodeConnectionFactory); + if (cf == null) { + this.logger.warn("Failed to determine queue location for: " + queue + ", using default connection factory"); } - this.logger.warn("Failed to determine queue location for: " + queue + ", using default connection factory"); - return null; - } - - /** - * Create a client instance. - * @param adminUri the admin URI. - * @param username the username - * @param password the password. - * @return The client. - * @throws MalformedURLException if the URL is malformed - * @throws URISyntaxException if there is a syntax error. - */ - protected Client createClient(String adminUri, String username, String password) throws MalformedURLException, - URISyntaxException { - return new Client(adminUri, username, password); + return cf; } - private synchronized ConnectionFactory nodeConnectionFactory(String queue, String node, String address) { - if (this.logger.isInfoEnabled()) { - this.logger.info("Queue: " + queue + " is on node: " + node + " at: " + address); - } - ConnectionFactory cf = this.nodeFactories.get(node); - if (cf == null) { - cf = createConnectionFactory(address, node); + private ConnectionFactory nodeConnectionFactory(@Nullable String queue, String node, String address) { + this.lock.lock(); + try { if (this.logger.isInfoEnabled()) { - this.logger.info("Created new connection factory: " + cf); + this.logger.info("Queue: " + queue + " is on node: " + node + " at: " + address); + } + ConnectionFactory cf = this.nodeFactories.get(node); + if (cf == null) { + cf = createConnectionFactory(address, node); + if (this.logger.isInfoEnabled()) { + this.logger.info("Created new connection factory: " + cf); + } + this.nodeFactories.put(node, cf); } - this.nodeFactories.put(node, cf); + return cf; + } + finally { + this.lock.unlock(); } - return cf; } /** @@ -350,12 +353,12 @@ protected ConnectionFactory createConnectionFactory(String address, String node) } @Override - public void destroy() { + public void resetConnection() { Exception lastException = null; for (ConnectionFactory connectionFactory : this.nodeFactories.values()) { - if (connectionFactory instanceof DisposableBean) { + if (connectionFactory instanceof DisposableBean disposable) { try { - ((DisposableBean) connectionFactory).destroy(); + disposable.destroy(); } catch (Exception e) { lastException = e; @@ -367,4 +370,9 @@ public void destroy() { } } + @Override + public void destroy() { + resetConnection(); + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java new file mode 100644 index 0000000000..4b4846f6ce --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java @@ -0,0 +1,123 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.connection; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; + +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpException; +import org.springframework.core.log.LogAccessor; + +/** + * Used to obtain a connection factory for the queue leader. + * @param the client type. + * + * @author Gary Russell + * @author Artem Bilan + * + * @since 2.4.8 + */ +public interface NodeLocator { + + LogAccessor LOGGER = new LogAccessor(LogFactory.getLog(NodeLocator.class)); + + /** + * Return a connection factory for the leader node for the queue. + * @param adminUris an array of admin URIs. + * @param nodeToAddress a map of node names to node addresses (AMQP). + * @param vhost the vhost. + * @param username the username. + * @param password the password. + * @param queue the queue name. + * @param factoryFunction an internal function to find or create the factory. + * @return a connection factory, if the leader node was found; null otherwise. + */ + default @Nullable ConnectionFactory locate(String[] adminUris, Map nodeToAddress, + String vhost, String username, String password, String queue, + FactoryFinder factoryFunction) { + + T client = createClient(username, password); + + for (String uris : adminUris) { + String adminUri = uris; + if (!adminUri.endsWith("/api/")) { + adminUri += "/api/"; + } + try { + String uri = new URI(adminUri) + .resolve("/api/queues/").toString(); + Map queueInfo = restCall(client, uri, vhost, queue); + if (queueInfo != null) { + String node = (String) queueInfo.get("node"); + if (node != null) { + String nodeUri = nodeToAddress.get(node); + if (nodeUri != null) { + close(client); + return factoryFunction.locate(queue, node, nodeUri); + } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("No match for node: " + node); + } + } + } + else { + throw new AmqpException("Admin returned null QueueInfo"); + } + } + catch (Exception e) { + LOGGER.warn("Failed to determine queue location for: " + queue + " at: " + + adminUri + ": " + e.getMessage()); + } + } + LOGGER.warn("Failed to determine queue location for: " + queue + ", using default connection factory"); + close(client); + return null; + } + + /** + * Create a client for subsequent use. + * @param userName the username. + * @param password the password. + * @return the client. + */ + T createClient(String userName, String password); + + /** + * Close the client. + * @param client the client. + */ + default void close(T client) { + } + + /** + * Retrieve a map of queue properties using the RabbitMQ Management REST API. + * @param client the client. + * @param baseUri the base uri. + * @param vhost the virtual host. + * @param queue the queue name. + * @return the map of queue properties. + * @throws URISyntaxException if the syntax is bad. + */ + @Nullable + Map restCall(T client, String baseUri, String vhost, String queue) + throws URISyntaxException; + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java index 95c71fe744..2d65393e33 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Instances of this object track pending publisher confirms. @@ -27,6 +27,7 @@ * expired. It also holds {@link CorrelationData} for * the client to correlate a confirm with a sent message. * @author Gary Russell + * @author Ngoc Nhan * @since 1.0.1 * */ @@ -34,14 +35,13 @@ public class PendingConfirm { static final long RETURN_CALLBACK_TIMEOUT = 60; - @Nullable - private final CorrelationData correlationData; + private final @Nullable CorrelationData correlationData; private final long timestamp; private final CountDownLatch latch = new CountDownLatch(1); - private String cause; + private @Nullable String cause; private boolean returned; @@ -84,8 +84,7 @@ public void setCause(String cause) { * @return the cause. * @since 1.4 */ - @Nullable - public String getCause() { + public @Nullable String getCause() { return this.cause; } @@ -115,7 +114,7 @@ public void setReturned(boolean isReturned) { * @since 2.2.10 */ public boolean waitForReturnIfNeeded() throws InterruptedException { - return this.returned ? this.latch.await(RETURN_CALLBACK_TIMEOUT, TimeUnit.SECONDS) : true; + return !this.returned || this.latch.await(RETURN_CALLBACK_TIMEOUT, TimeUnit.SECONDS); } /** @@ -128,7 +127,8 @@ public void countDown() { @Override public String toString() { - return "PendingConfirm [correlationData=" + this.correlationData + (this.cause == null ? "" : " cause=" + this.cause) + "]"; + return "PendingConfirm [correlationData=" + this.correlationData + + (this.cause == null ? "" : " cause=" + this.cause) + "]"; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index 7187264860..b2aeac66aa 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,13 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.ShutdownListener; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.apache.commons.logging.Log; @@ -31,35 +36,43 @@ import org.apache.commons.pool2.PooledObjectFactory; import org.apache.commons.pool2.impl.DefaultPooledObject; import org.apache.commons.pool2.impl.GenericObjectPool; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.NameMatchMethodPointcutAdvisor; -import org.springframework.lang.Nullable; +import org.springframework.context.SmartLifecycle; import org.springframework.util.Assert; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.ShutdownListener; - /** * A very simple connection factory that caches channels using Apache Pool2 * {@link GenericObjectPool}s (one for transactional and one for non-transactional - * channels). The pools have default configuration but they can be configured using + * channels). The pools have default configuration, but they can be configured using * a callback. * * @author Gary Russell + * @author Leonardo Ferreira + * @author Christian Tzolov + * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.3 * */ -public class PooledChannelConnectionFactory extends AbstractConnectionFactory implements ShutdownListener { +public class PooledChannelConnectionFactory extends AbstractConnectionFactory + implements ShutdownListener, SmartLifecycle { + + private final AtomicBoolean running = new AtomicBoolean(); - private volatile ConnectionWrapper connection; + private final Lock lock = new ReentrantLock(); + + private volatile @Nullable ConnectionWrapper connection; private boolean simplePublisherConfirms; - private BiConsumer, Boolean> poolConfigurer = (pool, tx) -> { }; + private BiConsumer, Boolean> poolConfigurer = (pool, tx) -> { + }; private boolean defaultPublisherFactory = true; @@ -76,10 +89,11 @@ public PooledChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory) * @param rabbitConnectionFactory the rabbitmq connection factory. * @param isPublisher true if we are creating a publisher connection factory. */ + @SuppressWarnings("this-escape") private PooledChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory, boolean isPublisher) { super(rabbitConnectionFactory); if (!isPublisher) { - setPublisherConnectionFactory(new PooledChannelConnectionFactory(rabbitConnectionFactory, true)); + doSetPublisherConnectionFactory(new PooledChannelConnectionFactory(rabbitConnectionFactory, true)); } else { this.defaultPublisherFactory = false; @@ -97,6 +111,7 @@ public void setPublisherConnectionFactory(@Nullable AbstractConnectionFactory pu * called with the transactional pool. * @param poolConfigurer the configurer. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setPoolConfigurer(BiConsumer, Boolean> poolConfigurer) { Assert.notNull(poolConfigurer, "'poolConfigurer' cannot be null"); this.poolConfigurer = poolConfigurer; // NOSONAR - sync inconsistency @@ -114,11 +129,12 @@ public boolean isSimplePublisherConfirms() { * Enable simple publisher confirms. * @param simplePublisherConfirms true to enable. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setSimplePublisherConfirms(boolean simplePublisherConfirms) { this.simplePublisherConfirms = simplePublisherConfirms; if (this.defaultPublisherFactory) { ((PooledChannelConnectionFactory) getPublisherConnectionFactory()) - .setSimplePublisherConfirms(simplePublisherConfirms); // NOSONAR + .setSimplePublisherConfirms(simplePublisherConfirms); // NOSONAR } } @@ -126,20 +142,53 @@ public void setSimplePublisherConfirms(boolean simplePublisherConfirms) { public void addConnectionListener(ConnectionListener listener) { super.addConnectionListener(listener); // handles publishing sub-factory // If the connection is already alive we assume that the new listener wants to be notified - if (this.connection != null && this.connection.isOpen()) { - listener.onCreate(this.connection); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null && connectionWrapper.isOpen()) { + listener.onCreate(connectionWrapper); } } @Override - public synchronized Connection createConnection() throws AmqpException { - if (this.connection == null || !this.connection.isOpen()) { - Connection bareConnection = createBareConnection(); // NOSONAR - see destroy() - this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout(), // NOSONAR - this.simplePublisherConfirms, this.poolConfigurer, getChannelListener()); // NOSONAR - getConnectionListener().onCreate(this.connection); + public int getPhase() { + return Integer.MIN_VALUE; + } + + @Override + public void start() { + this.running.set(true); + } + + @Override + public void stop() { + this.running.set(false); + resetConnection(); + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + + @Override + public Connection createConnection() throws AmqpException { + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper == null || !connectionWrapper.isOpen()) { + this.lock.lock(); + try { + connectionWrapper = this.connection; + if (connectionWrapper == null || !connectionWrapper.isOpen()) { + Connection bareConnection = createBareConnection(); + connectionWrapper = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout(), + this.simplePublisherConfirms, this.poolConfigurer, getChannelListener()); + this.connection = connectionWrapper; + getConnectionListener().onCreate(this.connection); + } + } + finally { + this.lock.unlock(); + } } - return this.connection; + return connectionWrapper; } /** @@ -148,17 +197,25 @@ public synchronized Connection createConnection() throws AmqpException { * used to force a reconnect to the primary broker after failing over to a secondary * broker. */ + @Override public void resetConnection() { destroy(); } @Override - public synchronized void destroy() { - super.destroy(); - if (this.connection != null) { - this.connection.forceClose(); - getConnectionListener().onClose(this.connection); - this.connection = null; + public void destroy() { + this.lock.lock(); + try { + super.destroy(); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null) { + connectionWrapper.forceClose(); + getConnectionListener().onClose(connectionWrapper); + this.connection = null; + } + } + finally { + this.lock.unlock(); } } @@ -178,16 +235,20 @@ private static final class ConnectionWrapper extends SimpleConnection { BiConsumer, Boolean> configurer, ChannelListener channelListener) { super(delegate, closeTimeout); - GenericObjectPool pool = new GenericObjectPool<>(new ChannelFactory()); - configurer.accept(pool, false); - this.channels = pool; - pool = new GenericObjectPool<>(new TxChannelFactory()); - configurer.accept(pool, true); - this.txChannels = pool; + this.channels = createPool(new ChannelFactory(), configurer, false); + this.txChannels = createPool(new TxChannelFactory(), configurer, true); this.simplePublisherConfirms = simplePublisherConfirms; this.channelListener = channelListener; } + private GenericObjectPool createPool(ChannelFactory channelFactory, + BiConsumer, Boolean> configurer, boolean tx) { + + GenericObjectPool pool = new GenericObjectPool<>(channelFactory); + configurer.accept(pool, tx); + return pool; + } + @Override public Channel createChannel(boolean transactional) { try { @@ -200,6 +261,7 @@ public Channel createChannel(boolean transactional) { } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private Channel createProxy(Channel channel, boolean transacted) { ProxyFactory pf = new ProxyFactory(channel); AtomicReference proxy = new AtomicReference<>(); @@ -207,21 +269,21 @@ private Channel createProxy(Channel channel, boolean transacted) { Advice advice = (MethodInterceptor) invocation -> { String method = invocation.getMethod().getName(); - switch (method) { - case "close": - handleClose(channel, transacted, proxy); - return null; - case "getTargetChannel": - return channel; - case "isTransactional": - return transacted; - case "confirmSelect": - confirmSelected.set(true); - return channel.confirmSelect(); - case "isConfirmSelected": - return confirmSelected.get(); - } - return null; + return switch (method) { + case "close" -> { + handleClose(channel, transacted, proxy); + yield null; + } + case "getTargetChannel" -> channel; + case "isTransactional" -> transacted; + case "confirmSelect" -> { + confirmSelected.set(true); + yield channel.confirmSelect(); + } + case "isConfirmSelected" -> confirmSelected.get(); + case "isPublisherConfirms" -> false; + default -> null; + }; }; NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(advice); advisor.addMethodName("close"); @@ -229,6 +291,7 @@ private Channel createProxy(Channel channel, boolean transacted) { advisor.addMethodName("isTransactional"); advisor.addMethodName("confirmSelect"); advisor.addMethodName("isConfirmSelected"); + advisor.addMethodName("isPublisherConfirms"); pf.addAdvisor(advisor); pf.addInterface(ChannelProxy.class); proxy.set((Channel) pf.getProxy()); @@ -291,7 +354,12 @@ public PooledObject makeObject() { @Override public void destroyObject(PooledObject p) throws Exception { - p.getObject().close(); + Channel channel = p.getObject(); + if (channel instanceof ChannelProxy channelProxy) { + channel = channelProxy.getTargetChannel(); + } + + ConnectionWrapper.this.physicalClose(channel); } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannel.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannel.java index e27eba7207..7370d017e7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannel.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannel.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -19,7 +19,6 @@ import java.util.Collection; import java.util.function.Consumer; -import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Return; @@ -115,37 +114,11 @@ interface Listener { */ void handleConfirm(PendingConfirm pendingConfirm, boolean ack); - /** - * Handle a returned message. - * @param replyCode the reply code. - * @param replyText the reply text. - * @param exchange the exchange. - * @param routingKey the routing key. - * @param properties the message properties. - * @param body the message body. - * @deprecated in favor of {@link #handleReturn(Return)}. - */ - @Deprecated - default void handleReturn(int replyCode, - String replyText, - String exchange, - String routingKey, - AMQP.BasicProperties properties, - byte[] body) { - - throw new UnsupportedOperationException( - "This should never be called; please open a GitHub issue with a stack trace"); - } - /** * Handle a returned message. * @param returned the message and metadata. */ - @SuppressWarnings("deprecation") - default void handleReturn(Return returned) { - handleReturn(returned.getReplyCode(), returned.getReplyText(), returned.getExchange(), - returned.getRoutingKey(), returned.getProperties(), returned.getBody()); - } + void handleReturn(Return returned); /** * When called, this listener should remove all references to the diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java index 01f710b2aa..9684c8505f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,19 +34,8 @@ import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeoutException; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.core.ReturnedMessage; -import org.springframework.amqp.rabbit.connection.CorrelationData.Confirm; -import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; -import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.AMQP.Basic.RecoverOk; @@ -80,6 +69,18 @@ import com.rabbitmq.client.ShutdownListener; import com.rabbitmq.client.ShutdownSignalException; import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.core.ReturnedMessage; +import org.springframework.amqp.rabbit.connection.CorrelationData.Confirm; +import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; +import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Channel wrapper to allow a single listener able to handle @@ -88,6 +89,8 @@ * @author Gary Russell * @author Arnaud Cogoluègnes * @author Artem Bilan + * @author Christian Tzolov + * @author Ngoc Nhan * * @since 1.0.1 * @@ -99,6 +102,8 @@ public class PublisherCallbackChannelImpl private final Log logger = LogFactory.getLog(this.getClass()); + private final Lock lock = new ReentrantLock(); + private final Channel delegate; private final ConcurrentMap listeners = new ConcurrentHashMap<>(); @@ -111,7 +116,7 @@ public class PublisherCallbackChannelImpl private final ExecutorService executor; - private volatile java.util.function.Consumer afterAckCallback; + private volatile java.util.function.@Nullable Consumer afterAckCallback; /** * Create a {@link PublisherCallbackChannelImpl} instance based on the provided @@ -119,6 +124,7 @@ public class PublisherCallbackChannelImpl * @param delegate the delegate channel. * @param executor the executor. */ + @SuppressWarnings("this-escape") public PublisherCallbackChannelImpl(Channel delegate, ExecutorService executor) { Assert.notNull(executor, "'executor' must not be null"); this.delegate = delegate; @@ -127,20 +133,21 @@ public PublisherCallbackChannelImpl(Channel delegate, ExecutorService executor) } @Override - public synchronized void setAfterAckCallback(java.util.function.Consumer callback) { - if (getPendingConfirmsCount() == 0 && callback != null) { - callback.accept(this); + public void setAfterAckCallback(java.util.function.Consumer callback) { + this.lock.lock(); + try { + if (getPendingConfirmsCount() == 0) { + callback.accept(this); + } + else { + this.afterAckCallback = callback; + } } - else { - this.afterAckCallback = callback; + finally { + this.lock.unlock(); } } - -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// BEGIN PURE DELEGATE METHODS -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public void addShutdownListener(ShutdownListener listener) { this.delegate.addShutdownListener(listener); @@ -179,8 +186,8 @@ public Connection getConnection() { @Override public void close(int closeCode, String closeMessage) throws IOException, TimeoutException { this.delegate.close(closeCode, closeMessage); - if (this.delegate instanceof AutorecoveringChannel) { - ClosingRecoveryListener.removeChannel((AutorecoveringChannel) this.delegate); + if (this.delegate instanceof AutorecoveringChannel auto) { + ClosingRecoveryListener.removeChannel(auto); } } @@ -721,8 +728,14 @@ public boolean removeReturnListener(ReturnListener listener) { } @Override - public synchronized void clearReturnListeners() { - this.delegate.clearReturnListeners(); + public void clearReturnListeners() { + this.lock.lock(); + try { + this.delegate.clearReturnListeners(); + } + finally { + this.lock.unlock(); + } } @Override @@ -800,10 +813,6 @@ public Channel getDelegate() { return this.delegate; } -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// END PURE DELEGATE METHODS -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public void close() throws IOException, TimeoutException { if (this.logger.isDebugEnabled()) { @@ -824,42 +833,60 @@ private void shutdownCompleted(String cause) { this.executor.execute(() -> generateNacksForPendingAcks(cause)); } - private synchronized void generateNacksForPendingAcks(String cause) { - for (Entry> entry : this.pendingConfirms.entrySet()) { - Listener listener = entry.getKey(); - for (Entry confirmEntry : entry.getValue().entrySet()) { - confirmEntry.getValue().setCause(cause); - if (this.logger.isDebugEnabled()) { - this.logger.debug(this.toString() + " PC:Nack:(close):" + confirmEntry.getKey()); + private void generateNacksForPendingAcks(String cause) { + this.lock.lock(); + try { + for (Entry> entry : this.pendingConfirms.entrySet()) { + Listener listener = entry.getKey(); + for (Entry confirmEntry : entry.getValue().entrySet()) { + confirmEntry.getValue().setCause(cause); + if (this.logger.isDebugEnabled()) { + this.logger.debug(this + " PC:Nack:(close):" + confirmEntry.getKey()); + } + processAck(confirmEntry.getKey(), false, false, false); } - processAck(confirmEntry.getKey(), false, false, false); + listener.revoke(this); + } + if (this.logger.isDebugEnabled()) { + this.logger.debug("PendingConfirms cleared"); } - listener.revoke(this); + this.pendingConfirms.clear(); + this.listenerForSeq.clear(); + this.listeners.clear(); } - if (this.logger.isDebugEnabled()) { - this.logger.debug("PendingConfirms cleared"); + finally { + this.lock.unlock(); } - this.pendingConfirms.clear(); - this.listenerForSeq.clear(); - this.listeners.clear(); } @Override - public synchronized int getPendingConfirmsCount(Listener listener) { - SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); - if (pendingConfirmsForListener == null) { - return 0; + public int getPendingConfirmsCount(Listener listener) { + this.lock.lock(); + try { + SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); + if (pendingConfirmsForListener == null) { + return 0; + } + else { + return pendingConfirmsForListener.entrySet().size(); + } } - else { - return pendingConfirmsForListener.entrySet().size(); + finally { + this.lock.unlock(); } } @Override - public synchronized int getPendingConfirmsCount() { - return this.pendingConfirms.values().stream() - .mapToInt(Map::size) - .sum(); + public int getPendingConfirmsCount() { + this.lock.lock(); + try { + return this.pendingConfirms.values().stream() + .mapToInt(Map::size) + .sum(); + } + finally { + this.lock.unlock(); + } } /** @@ -869,12 +896,12 @@ public synchronized int getPendingConfirmsCount() { @Override public void addListener(Listener listener) { Assert.notNull(listener, "Listener cannot be null"); - if (this.listeners.size() == 0) { + if (this.listeners.isEmpty()) { this.delegate.addConfirmListener(this); this.delegate.addReturnListener(this); } if (this.listeners.putIfAbsent(listener.getUUID(), listener) == null) { - this.pendingConfirms.put(listener, new ConcurrentSkipListMap()); + this.pendingConfirms.put(listener, new ConcurrentSkipListMap<>()); if (this.logger.isDebugEnabled()) { this.logger.debug("Added listener " + listener); } @@ -882,13 +909,15 @@ public void addListener(Listener listener) { } @Override - public synchronized Collection expire(Listener listener, long cutoffTime) { - SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); - if (pendingConfirmsForListener == null) { - return Collections.emptyList(); - } - else { - List expired = new ArrayList(); + public Collection expire(Listener listener, long cutoffTime) { + this.lock.lock(); + try { + SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); + if (pendingConfirmsForListener == null) { + return Collections.emptyList(); + } + + List expired = new ArrayList<>(); Iterator> iterator = pendingConfirmsForListener.entrySet().iterator(); while (iterator.hasNext()) { PendingConfirm pendingConfirm = iterator.next().getValue(); @@ -906,6 +935,9 @@ public synchronized Collection expire(Listener listener, long cu } return expired; } + finally { + this.lock.unlock(); + } } // ConfirmListener @@ -913,7 +945,7 @@ public synchronized Collection expire(Listener listener, long cu @Override public void handleAck(long seq, boolean multiple) { if (this.logger.isDebugEnabled()) { - this.logger.debug(this.toString() + " PC:Ack:" + seq + ":" + multiple); + this.logger.debug(this + " PC:Ack:" + seq + ":" + multiple); } processAck(seq, true, multiple, true); } @@ -921,17 +953,23 @@ public void handleAck(long seq, boolean multiple) { @Override public void handleNack(long seq, boolean multiple) { if (this.logger.isDebugEnabled()) { - this.logger.debug(this.toString() + " PC:Nack:" + seq + ":" + multiple); + this.logger.debug(this + " PC:Nack:" + seq + ":" + multiple); } processAck(seq, false, multiple, true); } - private synchronized void processAck(long seq, boolean ack, boolean multiple, boolean remove) { + private void processAck(long seq, boolean ack, boolean multiple, boolean remove) { + this.lock.lock(); try { - doProcessAck(seq, ack, multiple, remove); + try { + doProcessAck(seq, ack, multiple, remove); + } + catch (Exception e) { + this.logger.error("Failed to process publisher confirm", e); + } } - catch (Exception e) { - this.logger.error("Failed to process publisher confirm", e); + finally { + this.lock.unlock(); } } @@ -955,7 +993,7 @@ private void doProcessAck(long seq, boolean ack, boolean multiple, boolean remov if (pendingConfirm != null) { CorrelationData correlationData = pendingConfirm.getCorrelationData(); if (correlationData != null) { - correlationData.getFuture().set(new Confirm(ack, pendingConfirm.getCause())); + correlationData.getFuture().complete(new Confirm(ack, pendingConfirm.getCause())); if (StringUtils.hasText(correlationData.getId())) { this.pendingReturns.remove(correlationData.getId()); // NOSONAR never null } @@ -973,13 +1011,13 @@ private void doProcessAck(long seq, boolean ack, boolean multiple, boolean remov private void processMultipleAck(long seq, boolean ack) { /* - * Piggy-backed ack - extract all Listeners for this and earlier + * Piggybacked ack - extract all Listeners for this and earlier * sequences. Then, for each Listener, handle each of it's acks. * Finally, remove the sequences from listenerForSeq. */ Map involvedListeners = this.listenerForSeq.headMap(seq + 1); // eliminate duplicates - Set listenersForAcks = new HashSet(involvedListeners.values()); + Set listenersForAcks = new HashSet<>(involvedListeners.values()); for (Listener involvedListener : listenersForAcks) { // find all unack'd confirms for this listener and handle them SortedMap confirmsMap = this.pendingConfirms.get(involvedListener); @@ -991,7 +1029,7 @@ private void processMultipleAck(long seq, boolean ack) { PendingConfirm value = entry.getValue(); CorrelationData correlationData = value.getCorrelationData(); if (correlationData != null) { - correlationData.getFuture().set(new Confirm(ack, value.getCause())); + correlationData.getFuture().complete(new Confirm(ack, value.getCause())); if (StringUtils.hasText(correlationData.getId())) { this.pendingReturns.remove(correlationData.getId()); // NOSONAR never null } @@ -1001,7 +1039,7 @@ private void processMultipleAck(long seq, boolean ack) { } } } - List seqs = new ArrayList(involvedListeners.keySet()); + List seqs = new ArrayList<>(involvedListeners.keySet()); for (Long key : seqs) { this.listenerForSeq.remove(key); } @@ -1028,12 +1066,18 @@ private void doHandleConfirm(boolean ack, Listener listener, PendingConfirm pend try { if (this.afterAckCallback != null) { java.util.function.Consumer callback = null; - synchronized (this) { + this.lock.lock(); + try { if (getPendingConfirmsCount() == 0) { callback = this.afterAckCallback; this.afterAckCallback = null; } + + } + finally { + this.lock.unlock(); } + if (callback != null) { callback.accept(this); } @@ -1048,18 +1092,24 @@ private void doHandleConfirm(boolean ack, Listener listener, PendingConfirm pend } @Override - public synchronized void addPendingConfirm(Listener listener, long seq, PendingConfirm pendingConfirm) { - SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); - Assert.notNull(pendingConfirmsForListener, - "Listener not registered: " + listener + " " + this.pendingConfirms.keySet()); - pendingConfirmsForListener.put(seq, pendingConfirm); - this.listenerForSeq.put(seq, listener); - if (pendingConfirm.getCorrelationData() != null) { - String returnCorrelation = pendingConfirm.getCorrelationData().getId(); // NOSONAR never null - if (StringUtils.hasText(returnCorrelation)) { - this.pendingReturns.put(returnCorrelation, pendingConfirm); + public void addPendingConfirm(Listener listener, long seq, PendingConfirm pendingConfirm) { + this.lock.lock(); + try { + SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); + Assert.notNull(pendingConfirmsForListener, + "Listener not registered: " + listener + " " + this.pendingConfirms.keySet()); + pendingConfirmsForListener.put(seq, pendingConfirm); + this.listenerForSeq.put(seq, listener); + if (pendingConfirm.getCorrelationData() != null) { + String returnCorrelation = pendingConfirm.getCorrelationData().getId(); // NOSONAR never null + if (StringUtils.hasText(returnCorrelation)) { + this.pendingReturns.put(returnCorrelation, pendingConfirm); + } } } + finally { + this.lock.unlock(); + } } // ReturnListener @@ -1068,7 +1118,7 @@ public synchronized void addPendingConfirm(Listener listener, long seq, PendingC public void handle(Return returned) { if (this.logger.isDebugEnabled()) { - this.logger.debug("Return " + this.toString()); + this.logger.debug("Return " + this); } PendingConfirm confirm = findConfirm(returned); Listener listener = findListener(returned.getProperties()); @@ -1081,26 +1131,23 @@ public void handle(Return returned) { if (confirm != null) { confirm.setReturned(true); } - Listener listenerToInvoke = listener; - PendingConfirm toCountDown = confirm; this.executor.execute(() -> { try { - listenerToInvoke.handleReturn(returned); + listener.handleReturn(returned); } catch (Exception e) { this.logger.error("Exception delivering returned message ", e); } finally { - if (toCountDown != null) { - toCountDown.countDown(); + if (confirm != null) { + confirm.countDown(); } } }); } } - @Nullable - private PendingConfirm findConfirm(Return returned) { + private @Nullable PendingConfirm findConfirm(Return returned) { LongString returnCorrelation = (LongString) returned.getProperties().getHeaders() .get(RETURNED_MESSAGE_CORRELATION_KEY); PendingConfirm confirm = null; @@ -1120,8 +1167,7 @@ private PendingConfirm findConfirm(Return returned) { return confirm; } - @Nullable - private Listener findListener(AMQP.BasicProperties properties) { + private @Nullable Listener findListener(AMQP.BasicProperties properties) { Listener listener = null; Object returnListenerHeader = properties.getHeaders().get(RETURN_LISTENER_CORRELATION_KEY); String uuidObject = null; @@ -1141,7 +1187,8 @@ private Listener findListener(AMQP.BasicProperties properties) { @Override public void shutdownCompleted(ShutdownSignalException cause) { - shutdownCompleted(cause.getMessage()); + String causeMessage = cause.getMessage(); + shutdownCompleted(causeMessage != null ? causeMessage : "Normal"); } // Object @@ -1159,11 +1206,11 @@ public boolean equals(Object obj) { @Override public String toString() { - return "PublisherCallbackChannelImpl: " + this.delegate.toString(); + return "PublisherCallbackChannelImpl: " + this.delegate; } public static PublisherCallbackChannelFactory factory() { - return (channel, exec) -> new PublisherCallbackChannelImpl(channel, exec); + return PublisherCallbackChannelImpl::new; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java index afebee61bc..5ccc9e8242 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,30 +16,36 @@ package org.springframework.amqp.rabbit.connection; +import com.rabbitmq.client.Channel; +import io.micrometer.observation.ObservationRegistry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationContext; import org.springframework.util.Assert; -import com.rabbitmq.client.Channel; - /** * @author Mark Fisher * @author Dave Syer * @author Gary Russell + * @author Artem Bilan */ public abstract class RabbitAccessor implements InitializingBean { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR + @SuppressWarnings("NullAway.Init") private volatile ConnectionFactory connectionFactory; private volatile boolean transactional; + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + public boolean isChannelTransacted() { return this.transactional; } @@ -89,8 +95,7 @@ protected Connection createConnection() { * @param holder the RabbitResourceHolder * @return an appropriate Connection fetched from the holder, or null if none found */ - @Nullable - protected Connection getConnection(RabbitResourceHolder holder) { + protected @Nullable Connection getConnection(RabbitResourceHolder holder) { return holder.getConnection(); } @@ -100,8 +105,7 @@ protected Connection getConnection(RabbitResourceHolder holder) { * @param holder the RabbitResourceHolder * @return an appropriate Channel fetched from the holder, or null if none found */ - @Nullable - protected Channel getChannel(RabbitResourceHolder holder) { + protected @Nullable Channel getChannel(RabbitResourceHolder holder) { return holder.getChannel(); } @@ -113,4 +117,16 @@ protected RuntimeException convertRabbitAccessException(Exception ex) { return RabbitExceptionTranslator.convertRabbitAccessException(ex); } + protected void obtainObservationRegistry(@Nullable ApplicationContext appContext) { + if (appContext != null) { + ObjectProvider registry = + appContext.getBeanProvider(ObservationRegistry.class); + this.observationRegistry = registry.getIfUnique(() -> this.observationRegistry); + } + } + + protected ObservationRegistry getObservationRegistry() { + return this.observationRegistry; + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java index 08b1dfd470..9b276b8b50 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,15 +40,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; -import org.springframework.beans.factory.config.AbstractFactoryBean; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.ExceptionHandler; import com.rabbitmq.client.MetricsCollector; @@ -57,16 +48,25 @@ import com.rabbitmq.client.impl.CredentialsProvider; import com.rabbitmq.client.impl.CredentialsRefreshService; import com.rabbitmq.client.impl.nio.NioParams; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Factory bean to create a RabbitMQ ConnectionFactory, delegating most setter methods and * optionally enabling SSL, with or without certificate validation. When * {@link #setSslPropertiesLocation(Resource) sslPropertiesLocation} is not null, the * default implementation loads a {@code PKCS12} keystore and a {@code JKS} truststore - * using the supplied properties and intializes key and trust manager factories, using + * using the supplied properties and initializes key and trust manager factories, using * algorithm {@code SunX509} by default. These are then used to initialize an * {@link SSLContext} using the {@link #setSslAlgorithm(String) sslAlgorithm} (default - * TLSv1.1). + * TLSv1.2, falling back to TLSv1.1, if 1.2 is not available). *

* Override {@link #createSSLContext()} to create and/or perform further modification of * the context. @@ -79,6 +79,7 @@ * @author Hareendran * @author Dominique Villard * @author Zachary DeLuca + * @author Ngoc Nhan * * @since 1.4 */ @@ -129,29 +130,29 @@ public class RabbitConnectionFactoryBean extends AbstractFactoryBean never be set to true in production * @param skipServerCertificateValidation Flag to override Server side certificate checks; @@ -234,15 +235,15 @@ protected String getSslAlgorithm() { * set property in this bean. * @param sslPropertiesLocation the Resource to the ssl properties */ - public void setSslPropertiesLocation(Resource sslPropertiesLocation) { + public void setSslPropertiesLocation(@Nullable Resource sslPropertiesLocation) { this.sslPropertiesLocation = sslPropertiesLocation; } /** - * @return the properties location. + * @return the properties file location. * @since 1.4.4 */ - protected Resource getSslPropertiesLocation() { + protected @Nullable Resource getSslPropertiesLocation() { return this.sslPropertiesLocation; } @@ -262,11 +263,11 @@ protected String getKeyStore() { * @param keyStore the keystore resource. * @since 1.5 */ - public void setKeyStore(String keyStore) { + public void setKeyStore(@Nullable String keyStore) { this.keyStore = keyStore; } - protected Resource getKeyStoreResource() { + protected @Nullable Resource getKeyStoreResource() { return this.keyStoreResource; } @@ -295,11 +296,11 @@ protected String getTrustStore() { * @param trustStore the truststore resource. * @since 1.5 */ - public void setTrustStore(String trustStore) { + public void setTrustStore(@Nullable String trustStore) { this.trustStore = trustStore; } - protected Resource getTrustStoreResource() { + protected @Nullable Resource getTrustStoreResource() { return this.trustStoreResource; } @@ -316,7 +317,7 @@ public void setTrustStoreResource(Resource trustStoreResource) { * @return the key store pass phrase. * @since 1.5 */ - protected String getKeyStorePassphrase() { + protected @Nullable String getKeyStorePassphrase() { return this.keyStorePassphrase == null ? this.sslProperties.getProperty(KEY_STORE_PASS_PHRASE) : this.keyStorePassphrase; } @@ -327,7 +328,7 @@ protected String getKeyStorePassphrase() { * @param keyStorePassphrase the key store pass phrase. * @since 1.5 */ - public void setKeyStorePassphrase(String keyStorePassphrase) { + public void setKeyStorePassphrase(@Nullable String keyStorePassphrase) { this.keyStorePassphrase = keyStorePassphrase; } @@ -335,7 +336,7 @@ public void setKeyStorePassphrase(String keyStorePassphrase) { * @return the trust store pass phrase. * @since 1.5 */ - protected String getTrustStorePassphrase() { + protected @Nullable String getTrustStorePassphrase() { return this.trustStorePassphrase == null ? this.sslProperties.getProperty(TRUST_STORE_PASS_PHRASE) : this.trustStorePassphrase; } @@ -346,7 +347,7 @@ protected String getTrustStorePassphrase() { * @param trustStorePassphrase the trust store pass phrase. * @since 1.5 */ - public void setTrustStorePassphrase(String trustStorePassphrase) { + public void setTrustStorePassphrase(@Nullable String trustStorePassphrase) { this.trustStorePassphrase = trustStorePassphrase; } @@ -360,12 +361,11 @@ protected String getKeyStoreType() { if (this.keyStoreType == null && this.sslProperties.getProperty(KEY_STORE_TYPE) == null) { return KEY_STORE_DEFAULT_TYPE; } - else if (this.keyStoreType != null) { + if (this.keyStoreType != null) { return this.keyStoreType; } - else { - return this.sslProperties.getProperty(KEY_STORE_TYPE); - } + + return this.sslProperties.getProperty(KEY_STORE_TYPE); } /** @@ -375,7 +375,7 @@ else if (this.keyStoreType != null) { * @since 1.6.2 * @see java.security.KeyStore#getInstance(String) */ - public void setKeyStoreType(String keyStoreType) { + public void setKeyStoreType(@Nullable String keyStoreType) { this.keyStoreType = keyStoreType; } @@ -389,12 +389,11 @@ protected String getTrustStoreType() { if (this.trustStoreType == null && this.sslProperties.getProperty(TRUST_STORE_TYPE) == null) { return TRUST_STORE_DEFAULT_TYPE; } - else if (this.trustStoreType != null) { + if (this.trustStoreType != null) { return this.trustStoreType; } - else { - return this.sslProperties.getProperty(TRUST_STORE_TYPE); - } + + return this.sslProperties.getProperty(TRUST_STORE_TYPE); } /** @@ -404,11 +403,11 @@ else if (this.trustStoreType != null) { * @since 1.6.2 * @see java.security.KeyStore#getInstance(String) */ - public void setTrustStoreType(String trustStoreType) { + public void setTrustStoreType(@Nullable String trustStoreType) { this.trustStoreType = trustStoreType; } - protected SecureRandom getSecureRandom() { + protected @Nullable SecureRandom getSecureRandom() { return this.secureRandom; } @@ -440,7 +439,7 @@ public void setPort(int port) { } /** - * @param username the user name. + * @param username the username. * @see com.rabbitmq.client.ConnectionFactory#setUsername(java.lang.String) */ public void setUsername(String username) { @@ -597,7 +596,7 @@ public void setExceptionHandler(ExceptionHandler exceptionHandler) { } /** - * Whether or not the factory should be configured to use Java NIO. + * Whether the factory should be configured to use Java NIO. * @param useNio true to use Java NIO, false to use blocking IO * @see com.rabbitmq.client.ConnectionFactory#useNio() */ @@ -672,6 +671,16 @@ public void setEnableHostnameVerification(boolean enable) { this.enableHostnameVerification = enable; } + /** + * Set the maximum body size of inbound (received) messages in bytes. + * @param maxInboundMessageBodySize the maximum size. + * @since 2.4.15 + * @see com.rabbitmq.client.ConnectionFactory#setMaxInboundMessageBodySize(int) + */ + public void setMaxInboundMessageBodySize(int maxInboundMessageBodySize) { + this.connectionFactory.setMaxInboundMessageBodySize(maxInboundMessageBodySize); + } + protected String getKeyStoreAlgorithm() { return this.keyStoreAlgorithm; } @@ -802,8 +811,8 @@ private void setupBasicSSL() throws NoSuchAlgorithmException, KeyManagementExcep } } - @Nullable - protected KeyManager[] configureKeyManagers() throws KeyStoreException, IOException, NoSuchAlgorithmException, + protected KeyManager @Nullable [] configureKeyManagers() + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { String keyStoreName = getKeyStore(); String keyStorePassword = getKeyStorePassphrase(); @@ -827,8 +836,7 @@ protected KeyManager[] configureKeyManagers() throws KeyStoreException, IOExcept return keyManagers; } - @Nullable - protected TrustManager[] configureTrustManagers() + protected TrustManager @Nullable [] configureTrustManagers() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { String trustStoreName = getTrustStore(); String trustStorePassword = getTrustStorePassphrase(); @@ -863,7 +871,6 @@ protected SSLContext createSSLContext() throws NoSuchAlgorithmException { return SSLContext.getInstance(this.sslAlgorithm); } - private void useDefaultTrustStoreMechanism() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { SSLContext sslContext = SSLContext.getInstance(this.sslAlgorithm); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java index 8cea93e04e..9824477081 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,18 +22,17 @@ import java.util.List; import java.util.Map; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.AmqpIOException; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; - -import com.rabbitmq.client.Channel; /** * Rabbit resource holder, wrapping a RabbitMQ Connection and Channel. * RabbitTransactionManager binds instances of this @@ -45,6 +44,7 @@ * @author Mark Fisher * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * * @see org.springframework.amqp.rabbit.transaction.RabbitTransactionManager * @see org.springframework.amqp.rabbit.core.RabbitTemplate @@ -118,11 +118,8 @@ public final void addChannel(Channel channel, @Nullable Connection connection) { if (!this.channels.contains(channel)) { this.channels.add(channel); if (connection != null) { - List channelsForConnection = this.channelsPerConnection.get(connection); - if (channelsForConnection == null) { - channelsForConnection = new LinkedList(); - this.channelsPerConnection.put(connection, channelsForConnection); - } + List channelsForConnection = + this.channelsPerConnection.computeIfAbsent(connection, k -> new LinkedList<>()); channelsForConnection.add(channel); } } @@ -132,13 +129,11 @@ public boolean containsChannel(Channel channel) { return this.channels.contains(channel); } - @Nullable - public Connection getConnection() { + public @Nullable Connection getConnection() { return (!this.connections.isEmpty() ? this.connections.get(0) : null); } - @Nullable - public Channel getChannel() { + public @Nullable Channel getChannel() { return (!this.channels.isEmpty() ? this.channels.get(0) : null); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java index d52e769ca8..13502ab92e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,6 @@ import java.io.IOException; import java.util.Collection; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.amqp.AmqpIOException; -import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - import com.rabbitmq.client.AMQP; import com.rabbitmq.client.AlreadyClosedException; import com.rabbitmq.client.Channel; @@ -37,6 +29,13 @@ import com.rabbitmq.client.ShutdownSignalException; import com.rabbitmq.client.impl.CRDemoMechanism; import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpIOException; +import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; +import org.springframework.util.Assert; /** * @author Mark Fisher @@ -83,12 +82,12 @@ public abstract class RabbitUtils { private static final Log LOGGER = LogFactory.getLog(RabbitUtils.class); - private static final ThreadLocal physicalCloseRequired = new ThreadLocal<>(); // NOSONAR - lower case + private static final ThreadLocal<@Nullable Boolean> physicalCloseRequired = new ThreadLocal<>(); // NOSONAR - lower case /** * Close the given RabbitMQ Connection and ignore any thrown exception. This is useful for typical * finally blocks in manual RabbitMQ code. - * @param connection the RabbitMQ Connection to close (may be null) + * @param connection the RabbitMQ Connection to close (maybe null) */ public static void closeConnection(@Nullable Connection connection) { if (connection != null) { @@ -107,7 +106,7 @@ public static void closeConnection(@Nullable Connection connection) { /** * Close the given RabbitMQ Channel and ignore any thrown exception. This is useful for typical finally * blocks in manual RabbitMQ code. - * @param channel the RabbitMQ Channel to close (may be null) + * @param channel the RabbitMQ Channel to close (maybe null) */ public static void closeChannel(@Nullable Channel channel) { if (channel != null) { @@ -156,8 +155,8 @@ public static void rollbackIfNecessary(Channel channel) { } public static void closeMessageConsumer(Channel channel, Collection consumerTags, boolean transactional) { - if (!channel.isOpen() && !(channel instanceof ChannelProxy - && ((ChannelProxy) channel).getTargetChannel() instanceof AutorecoveringChannel) + if (!channel.isOpen() && !(channel instanceof ChannelProxy proxy + && proxy.getTargetChannel() instanceof AutorecoveringChannel) && !(channel instanceof AutorecoveringChannel)) { return; } @@ -228,12 +227,7 @@ public static void setPhysicalCloseRequired(Channel channel, boolean b) { */ public static boolean isPhysicalCloseRequired() { Boolean mustClose = physicalCloseRequired.get(); - if (mustClose == null) { - return false; - } - else { - return mustClose; - } + return mustClose != null && mustClose; } /** @@ -251,9 +245,9 @@ public static void clearPhysicalCloseRequired() { */ public static boolean isNormalShutdown(ShutdownSignalException sig) { Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Connection.Close - && AMQP.REPLY_SUCCESS == ((AMQP.Connection.Close) shutdownReason).getReplyCode() - && "OK".equals(((AMQP.Connection.Close) shutdownReason).getReplyText()); + return shutdownReason instanceof AMQP.Connection.Close closeReason + && AMQP.REPLY_SUCCESS == closeReason.getReplyCode() + && "OK".equals(closeReason.getReplyText()); } /** @@ -265,9 +259,9 @@ public static boolean isNormalShutdown(ShutdownSignalException sig) { public static boolean isNormalChannelClose(ShutdownSignalException sig) { Method shutdownReason = sig.getReason(); return isNormalShutdown(sig) || - (shutdownReason instanceof AMQP.Channel.Close - && AMQP.REPLY_SUCCESS == ((AMQP.Channel.Close) shutdownReason).getReplyCode() - && "OK".equals(((AMQP.Channel.Close) shutdownReason).getReplyText())); + (shutdownReason instanceof AMQP.Channel.Close closeReason + && AMQP.REPLY_SUCCESS == closeReason.getReplyCode() + && "OK".equals(closeReason.getReplyText())); } /** @@ -278,11 +272,11 @@ public static boolean isNormalChannelClose(ShutdownSignalException sig) { */ public static boolean isPassiveDeclarationChannelClose(ShutdownSignalException sig) { Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Channel.Close // NOSONAR boolean complexity - && AMQP.NOT_FOUND == ((AMQP.Channel.Close) shutdownReason).getReplyCode() - && ((((AMQP.Channel.Close) shutdownReason).getClassId() == EXCHANGE_CLASS_ID_40 - || ((AMQP.Channel.Close) shutdownReason).getClassId() == QUEUE_CLASS_ID_50) - && ((AMQP.Channel.Close) shutdownReason).getMethodId() == DECLARE_METHOD_ID_10); + return shutdownReason instanceof AMQP.Channel.Close closeReason // NOSONAR boolean complexity + && AMQP.NOT_FOUND == closeReason.getReplyCode() + && ((closeReason.getClassId() == EXCHANGE_CLASS_ID_40 + || closeReason.getClassId() == QUEUE_CLASS_ID_50) + && closeReason.getMethodId() == DECLARE_METHOD_ID_10); } /** @@ -294,11 +288,11 @@ public static boolean isPassiveDeclarationChannelClose(ShutdownSignalException s */ public static boolean isExclusiveUseChannelClose(ShutdownSignalException sig) { Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Channel.Close // NOSONAR boolean complexity - && AMQP.ACCESS_REFUSED == ((AMQP.Channel.Close) shutdownReason).getReplyCode() - && ((AMQP.Channel.Close) shutdownReason).getClassId() == BASIC_CLASS_ID_60 - && ((AMQP.Channel.Close) shutdownReason).getMethodId() == CONSUME_METHOD_ID_20 - && ((AMQP.Channel.Close) shutdownReason).getReplyText().contains("exclusive"); + return shutdownReason instanceof AMQP.Channel.Close closeReason // NOSONAR boolean complexity + && AMQP.ACCESS_REFUSED == closeReason.getReplyCode() + && closeReason.getClassId() == BASIC_CLASS_ID_60 + && closeReason.getMethodId() == CONSUME_METHOD_ID_20 + && closeReason.getReplyText().contains("exclusive"); } /** @@ -314,21 +308,20 @@ public static boolean isMismatchedQueueArgs(Exception e) { Throwable cause = e; ShutdownSignalException sig = null; while (cause != null && sig == null) { - if (cause instanceof ShutdownSignalException) { - sig = (ShutdownSignalException) cause; + if (cause instanceof ShutdownSignalException shutdownSignalException) { + sig = shutdownSignalException; } cause = cause.getCause(); } if (sig == null) { return false; } - else { - Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Channel.Close - && AMQP.PRECONDITION_FAILED == ((AMQP.Channel.Close) shutdownReason).getReplyCode() - && ((AMQP.Channel.Close) shutdownReason).getClassId() == QUEUE_CLASS_ID_50 - && ((AMQP.Channel.Close) shutdownReason).getMethodId() == DECLARE_METHOD_ID_10; - } + + Method shutdownReason = sig.getReason(); + return shutdownReason instanceof AMQP.Channel.Close closeReason + && AMQP.PRECONDITION_FAILED == closeReason.getReplyCode() + && closeReason.getClassId() == QUEUE_CLASS_ID_50 + && closeReason.getMethodId() == DECLARE_METHOD_ID_10; } /** @@ -344,21 +337,20 @@ public static boolean isExchangeDeclarationFailure(Exception e) { Throwable cause = e; ShutdownSignalException sig = null; while (cause != null && sig == null) { - if (cause instanceof ShutdownSignalException) { - sig = (ShutdownSignalException) cause; + if (cause instanceof ShutdownSignalException shutdownSignalException) { + sig = shutdownSignalException; } cause = cause.getCause(); } if (sig == null) { return false; } - else { - Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Connection.Close - && AMQP.COMMAND_INVALID == ((AMQP.Connection.Close) shutdownReason).getReplyCode() - && ((AMQP.Connection.Close) shutdownReason).getClassId() == EXCHANGE_CLASS_ID_40 - && ((AMQP.Connection.Close) shutdownReason).getMethodId() == DECLARE_METHOD_ID_10; - } + + Method shutdownReason = sig.getReason(); + return shutdownReason instanceof AMQP.Channel.Close closeReason + && AMQP.PRECONDITION_FAILED == closeReason.getReplyCode() + && closeReason.getClassId() == EXCHANGE_CLASS_ID_40 + && closeReason.getMethodId() == DECLARE_METHOD_ID_10; } /** @@ -395,18 +387,27 @@ public static int getMaxFrame(ConnectionFactory connectionFactory) { public static SaslConfig stringToSaslConfig(String saslConfig, com.rabbitmq.client.ConnectionFactory connectionFactory) { - switch (saslConfig) { - case "DefaultSaslConfig.PLAIN": - return DefaultSaslConfig.PLAIN; - case "DefaultSaslConfig.EXTERNAL": - return DefaultSaslConfig.EXTERNAL; - case "JDKSaslConfig": - return new JDKSaslConfig(connectionFactory); - case "CRDemoSaslConfig": - return new CRDemoMechanism.CRDemoSaslConfig(); - default: - throw new IllegalStateException("Unrecognized SaslConfig: " + saslConfig); - } + return switch (saslConfig) { + case "DefaultSaslConfig.PLAIN" -> DefaultSaslConfig.PLAIN; + case "DefaultSaslConfig.EXTERNAL" -> DefaultSaslConfig.EXTERNAL; + case "JDKSaslConfig" -> new JDKSaslConfig(connectionFactory); + case "CRDemoSaslConfig" -> new CRDemoMechanism.CRDemoSaslConfig(); + default -> throw new IllegalStateException("Unrecognized SaslConfig: " + saslConfig); + }; + } + + /** + * Determine whether the exception is due to an access refused for an exclusive consumer. + * @param exception the exception. + * @return true if access refused. + * @since 3.1 + */ + public static boolean exclusiveAccesssRefused(Exception exception) { + return exception.getCause() instanceof IOException + && exception.getCause().getCause() instanceof ShutdownSignalException sse1 + && isExclusiveUseChannelClose(sse1) + || exception.getCause() instanceof ShutdownSignalException sse2 + && isExclusiveUseChannelClose(sse2); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java new file mode 100644 index 0000000000..5f66d50d75 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java @@ -0,0 +1,84 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.connection; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hc.client5.http.auth.AuthCache; +import org.apache.hc.client5.http.impl.auth.BasicAuthCache; +import org.apache.hc.client5.http.impl.auth.BasicScheme; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.http.HttpHost; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriUtils; + +/** + * A {@link NodeLocator} using the {@link RestTemplate}. + * + * @author Gary Russell + * @author Artem Bilan + * + * @since 3.0 + * + */ +public class RestTemplateNodeLocator implements NodeLocator { + + private final AuthCache authCache = new BasicAuthCache(); + + private final AtomicBoolean authSchemeIsSetToCache = new AtomicBoolean(false); + + @Override + public RestTemplate createClient(String userName, String password) { + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpContextFactory((httpMethod, uri) -> { + HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(this.authCache); + return context; + }); + RestTemplate template = new RestTemplate(requestFactory); + template.getInterceptors().add(new BasicAuthenticationInterceptor(userName, password)); + return template; + } + + @Override + public @Nullable Map restCall(RestTemplate client, String baseUri, String vhost, String queue) { + + URI theBaseUri = URI.create(baseUri); + if (!this.authSchemeIsSetToCache.getAndSet(true)) { + this.authCache.put(HttpHost.create(theBaseUri), new BasicScheme()); + } + URI uri = theBaseUri + .resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" + queue); + ResponseEntity> response = + client.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference<>() { + + }); + return response.getStatusCode().equals(HttpStatus.OK) ? response.getBody() : null; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactory.java index 19cf40de13..08c7a43abf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or 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.amqp.rabbit.connection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Implementations select a connection factory based on a supplied key. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java index d627c333f4..40d31d53d9 100755 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,15 +19,18 @@ import java.io.IOException; import java.net.InetAddress; -import org.springframework.amqp.AmqpResourceNotAvailableException; -import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; -import org.springframework.util.ObjectUtils; - import com.rabbitmq.client.AlreadyClosedException; import com.rabbitmq.client.BlockedListener; import com.rabbitmq.client.Channel; import com.rabbitmq.client.impl.NetworkConnection; import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpResourceNotAvailableException; +import org.springframework.amqp.AmqpTimeoutException; +import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; +import org.springframework.util.ObjectUtils; +import org.springframework.util.backoff.BackOffExecution; /** * Simply a Connection. @@ -35,6 +38,7 @@ * @author Dave Syer * @author Gary Russell * @author Artem Bilan + * @author Salk Lee * * @since 1.0 */ @@ -46,16 +50,40 @@ public class SimpleConnection implements Connection, NetworkConnection { private volatile boolean explicitlyClosed; - public SimpleConnection(com.rabbitmq.client.Connection delegate, - int closeTimeout) { + @Nullable + private final BackOffExecution backOffExecution; + + public SimpleConnection(com.rabbitmq.client.Connection delegate, int closeTimeout) { + this(delegate, closeTimeout, null); + } + + /** + * Construct an instance with the {@link org.springframework.util.backoff.BackOffExecution} arguments. + * @param delegate delegate connection + * @param closeTimeout the time of physical close time out + * @param backOffExecution backOffExecution is nullable + * @since 3.1.3 + */ + public SimpleConnection(com.rabbitmq.client.Connection delegate, int closeTimeout, + @Nullable BackOffExecution backOffExecution) { + this.delegate = delegate; this.closeTimeout = closeTimeout; + this.backOffExecution = backOffExecution; } @Override public Channel createChannel(boolean transactional) { try { Channel channel = this.delegate.createChannel(); + while (channel == null && this.backOffExecution != null) { + long interval = this.backOffExecution.nextBackOff(); + if (interval == BackOffExecution.STOP) { + break; + } + Thread.sleep(interval); + channel = this.delegate.createChannel(); + } if (channel == null) { throw new AmqpResourceNotAvailableException("The channelMax limit is reached. Try later."); } @@ -65,6 +93,10 @@ public Channel createChannel(boolean transactional) { } return channel; } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AmqpTimeoutException("Interrupted while creating a new channel", e); + } catch (IOException e) { throw RabbitExceptionTranslator.convertRabbitAccessException(e); } @@ -103,14 +135,13 @@ public boolean isOpen() { if (!this.explicitlyClosed && this.delegate instanceof AutorecoveringConnection && !this.delegate.isOpen()) { throw new AutoRecoverConnectionNotCurrentlyOpenException("Auto recovery connection is not currently open"); } - return this.delegate != null && (this.delegate.isOpen()); + return this.delegate.isOpen(); } - @Override public int getLocalPort() { - if (this.delegate instanceof NetworkConnection) { - return ((NetworkConnection) this.delegate).getLocalPort(); + if (this.delegate instanceof NetworkConnection networkConn) { + return networkConn.getLocalPort(); } return 0; } @@ -126,9 +157,9 @@ public boolean removeBlockedListener(BlockedListener listener) { } @Override - public InetAddress getLocalAddress() { - if (this.delegate instanceof NetworkConnection) { - return ((NetworkConnection) this.delegate).getLocalAddress(); + public @Nullable InetAddress getLocalAddress() { + if (this.delegate instanceof NetworkConnection networkConn) { + return networkConn.getLocalAddress(); } return null; } @@ -152,7 +183,7 @@ public com.rabbitmq.client.Connection getDelegate() { public String toString() { return "SimpleConnection@" + ObjectUtils.getIdentityHexString(this) - + " [delegate=" + this.delegate + ", localPort= " + getLocalPort() + "]"; + + " [delegate=" + this.delegate + ", localPort=" + getLocalPort() + "]"; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimplePropertyValueConnectionNameStrategy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimplePropertyValueConnectionNameStrategy.java index 1a8287d9af..f4eebedd0f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimplePropertyValueConnectionNameStrategy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimplePropertyValueConnectionNameStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or 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.amqp.rabbit.connection; +import org.jspecify.annotations.Nullable; + import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.util.Assert; @@ -32,9 +34,9 @@ public class SimplePropertyValueConnectionNameStrategy implements ConnectionName private final String propertyName; - private String propertyValue; + private @Nullable String propertyValue; - private Environment environment; + private @Nullable Environment environment; public SimplePropertyValueConnectionNameStrategy(String propertyName) { Assert.notNull(propertyName, "'propertyName' cannot be null"); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java index bacdf4a61f..a31e0a2534 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -45,6 +45,8 @@ * * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan + * * @since 1.3 */ public final class SimpleResourceHolder { @@ -55,11 +57,11 @@ public final class SimpleResourceHolder { private static final Log LOGGER = LogFactory.getLog(SimpleResourceHolder.class); - private static final ThreadLocal> RESOURCES = - new NamedThreadLocal>("Simple resources"); + private static final ThreadLocal<@Nullable Map> RESOURCES = + new NamedThreadLocal<>("Simple resources"); - private static final ThreadLocal>> STACK = - new NamedThreadLocal>>("Simple resources"); + private static final ThreadLocal<@Nullable Map>> STACK = + new NamedThreadLocal<>("Simple resources"); /** * Return all resources that are bound to the current thread. @@ -91,8 +93,7 @@ public static boolean has(Object key) { * @return a value bound to the current thread (usually the active * resource object), or null if none */ - @Nullable - public static Object get(Object key) { + public static @Nullable Object get(Object key) { Object value = doGet(key); if (value != null && LOGGER.isTraceEnabled()) { LOGGER.trace("Retrieved value [" + value + FOR_KEY + key + BOUND_TO_THREAD @@ -106,8 +107,7 @@ public static Object get(Object key) { * @param actualKey the key. * @return the resource object. */ - @Nullable - private static Object doGet(Object actualKey) { + private static @Nullable Object doGet(Object actualKey) { Map map = RESOURCES.get(); if (map == null) { return null; @@ -126,7 +126,7 @@ public static void bind(Object key, Object value) { Map map = RESOURCES.get(); // set ThreadLocal Map if none found if (map == null) { - map = new HashMap(); + map = new HashMap<>(); RESOURCES.set(map); } Object oldValue = map.put(key, value); @@ -151,7 +151,7 @@ public static void push(Object key, Object value) { bind(key, value); } else { - Map> stack = STACK.get(); + Map> stack = STACK.get(); if (stack == null) { stack = new HashMap<>(); STACK.set(stack); @@ -169,13 +169,12 @@ public static void push(Object key, Object value) { * @return the popped value. * @since 2.1.11 */ - @Nullable public static Object pop(Object key) { Object popped = unbind(key); - Map> stack = STACK.get(); + Map> stack = STACK.get(); if (stack != null) { - Deque deque = stack.get(key); - if (deque != null && deque.size() > 0) { + Deque<@Nullable Object> deque = stack.get(key); + if (deque != null && !deque.isEmpty()) { Object previousValue = deque.pop(); if (previousValue != null) { bind(key, previousValue); @@ -206,8 +205,7 @@ public static Object unbind(Object key) throws IllegalStateException { * @param key the key to unbind (usually the resource factory) * @return the previously bound value, or null if none bound */ - @Nullable - public static Object unbindIfPossible(Object key) { + public static @Nullable Object unbindIfPossible(Object key) { Map map = RESOURCES.get(); if (map == null) { return null; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleRoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleRoutingConnectionFactory.java index 39a5bd3f24..cb11384782 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleRoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleRoutingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.rabbit.connection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An {@link AbstractRoutingConnectionFactory} implementation which gets a {@code lookupKey} @@ -25,13 +25,13 @@ * * @author Artem Bilan * @author Gary Russell + * * @since 1.3 */ public class SimpleRoutingConnectionFactory extends AbstractRoutingConnectionFactory { @Override - @Nullable - protected Object determineCurrentLookupKey() { + protected @Nullable Object determineCurrentLookupKey() { return SimpleResourceHolder.get(this); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index 35a28687d8..8795a5e509 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2025 the original author 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,38 +22,49 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.ShutdownListener; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.NameMatchMethodPointcutAdvisor; -import org.springframework.lang.Nullable; +import org.springframework.context.SmartLifecycle; import org.springframework.util.Assert; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.ShutdownListener; - /** * A very simple connection factory that caches a channel per thread. Users are * responsible for releasing the thread's channel by calling * {@link #closeThreadChannel()}. * * @author Gary Russell + * @author Leonardo Ferreira + * @author Christian Tzolov + * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.3 * */ -public class ThreadChannelConnectionFactory extends AbstractConnectionFactory implements ShutdownListener { +public class ThreadChannelConnectionFactory extends AbstractConnectionFactory + implements ShutdownListener, SmartLifecycle { + + private final Lock lock = new ReentrantLock(); private final Map contextSwitches = new ConcurrentHashMap<>(); private final Map switchesInProgress = new ConcurrentHashMap<>(); - private volatile ConnectionWrapper connection; + private final AtomicBoolean running = new AtomicBoolean(); + + private volatile @Nullable ConnectionWrapper connection; private boolean simplePublisherConfirms; @@ -72,6 +83,7 @@ public ThreadChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory) * @param rabbitConnectionFactory the rabbitmq connection factory. * @param isPublisher true if we are creating a publisher connection factory. */ + @SuppressWarnings("this-escape") private ThreadChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory, boolean isPublisher) { super(rabbitConnectionFactory); if (!isPublisher) { @@ -97,40 +109,74 @@ public boolean isSimplePublisherConfirms() { * Enable simple publisher confirms. * @param simplePublisherConfirms true to enable. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setSimplePublisherConfirms(boolean simplePublisherConfirms) { this.simplePublisherConfirms = simplePublisherConfirms; if (this.defaultPublisherFactory) { ((ThreadChannelConnectionFactory) getPublisherConnectionFactory()) - .setSimplePublisherConfirms(simplePublisherConfirms); // NOSONAR + .setSimplePublisherConfirms(simplePublisherConfirms); // NOSONAR } } + @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + + @Override + public void start() { + this.running.set(true); + } + + @Override + public void stop() { + this.running.set(false); + resetConnection(); + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + @Override public void addConnectionListener(ConnectionListener listener) { super.addConnectionListener(listener); // handles publishing sub-factory // If the connection is already alive we assume that the new listener wants to be notified - if (this.connection != null && this.connection.isOpen()) { - listener.onCreate(this.connection); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null && connectionWrapper.isOpen()) { + listener.onCreate(connectionWrapper); } } @Override - public synchronized Connection createConnection() throws AmqpException { - if (this.connection == null || !this.connection.isOpen()) { - Connection bareConnection = createBareConnection(); // NOSONAR - see destroy() - this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout()); - getConnectionListener().onCreate(this.connection); + public Connection createConnection() throws AmqpException { + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper == null || !connectionWrapper.isOpen()) { + this.lock.lock(); + try { + connectionWrapper = this.connection; + if (connectionWrapper == null || !connectionWrapper.isOpen()) { + Connection bareConnection = createBareConnection(); + connectionWrapper = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout()); + this.connection = connectionWrapper; + getConnectionListener().onCreate(this.connection); + } + } + finally { + this.lock.unlock(); + } } - return this.connection; + return connectionWrapper; } /** * Close the channel associated with this thread, if any. */ public void closeThreadChannel() { - ConnectionWrapper connection2 = this.connection; - if (connection2 != null) { - connection2.closeThreadChannel(); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null) { + connectionWrapper.closeThreadChannel(); } } @@ -140,26 +186,34 @@ public void closeThreadChannel() { * used to force a reconnect to the primary broker after failing over to a secondary * broker. */ + @Override public void resetConnection() { destroy(); } @Override - public synchronized void destroy() { - super.destroy(); - if (this.connection != null) { - this.connection.forceClose(); - this.connection = null; + public void destroy() { + this.lock.lock(); + try { + super.destroy(); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null) { + connectionWrapper.forceClose(); + this.connection = null; + } + if (!this.switchesInProgress.isEmpty() && this.logger.isWarnEnabled()) { + this.logger.warn("Unclaimed context switches from threads:" + + this.switchesInProgress.values() + .stream() + .map(Thread::getName) + .toList()); + } + this.contextSwitches.clear(); + this.switchesInProgress.clear(); } - if (this.switchesInProgress.size() > 0 && this.logger.isWarnEnabled()) { - this.logger.warn("Unclaimed context switches from threads:" + - this.switchesInProgress.values() - .stream() - .map(t -> t.getName()) - .collect(Collectors.toList())); + finally { + this.lock.unlock(); } - this.contextSwitches.clear(); - this.switchesInProgress.clear(); } /** @@ -169,23 +223,23 @@ public synchronized void destroy() { * @since 2.3.7 * @see #switchContext(Object) */ - @Nullable - public Object prepareSwitchContext() { + public @Nullable Object prepareSwitchContext() { return prepareSwitchContext(UUID.randomUUID()); } + @SuppressWarnings("resource") @Nullable Object prepareSwitchContext(UUID uuid) { Object pubContext = null; - if (getPublisherConnectionFactory() instanceof ThreadChannelConnectionFactory) { - pubContext = ((ThreadChannelConnectionFactory) getPublisherConnectionFactory()).prepareSwitchContext(uuid); // NOSONAR + if (getPublisherConnectionFactory() instanceof ThreadChannelConnectionFactory tccf) { + pubContext = tccf.prepareSwitchContext(uuid); } Context context = ((ConnectionWrapper) createConnection()).prepareSwitchContext(); - if (context.getNonTx() == null && context.getTx() == null) { + if (context.nonTx() == null && context.tx() == null) { this.logger.debug("No channels are bound to this thread"); return pubContext; } - if (this.switchesInProgress.values().contains(Thread.currentThread())) { + if (this.switchesInProgress.containsValue(Thread.currentThread())) { this.logger .warn("A previous context switch from this thread has not been claimed yet; possible memory leak?"); } @@ -210,10 +264,11 @@ public void switchContext(@Nullable Object toSwitch) { } } + @SuppressWarnings("resource") boolean doSwitch(Object toSwitch) { boolean switched = false; - if (getPublisherConnectionFactory() instanceof ThreadChannelConnectionFactory) { - switched = ((ThreadChannelConnectionFactory) getPublisherConnectionFactory()).doSwitch(toSwitch); // NOSONAR + if (getPublisherConnectionFactory() instanceof ThreadChannelConnectionFactory tccf) { + switched = tccf.doSwitch(toSwitch); // NOSONAR } Context context = this.contextSwitches.remove(toSwitch); this.switchesInProgress.remove(toSwitch); @@ -229,9 +284,9 @@ private final class ConnectionWrapper extends SimpleConnection { /* * Intentionally not static. */ - private final ThreadLocal channels = new ThreadLocal<>(); + private final ThreadLocal<@Nullable Channel> channels = new ThreadLocal<>(); - private final ThreadLocal txChannels = new ThreadLocal<>(); + private final ThreadLocal<@Nullable Channel> txChannels = new ThreadLocal<>(); ConnectionWrapper(com.rabbitmq.client.Connection delegate, int closeTimeout) { super(delegate, closeTimeout); @@ -274,21 +329,21 @@ private Channel createProxy(Channel channel, boolean transactional) { Advice advice = (MethodInterceptor) invocation -> { String method = invocation.getMethod().getName(); - switch (method) { - case "close": + return switch (method) { + case "close" -> { handleClose(channel, transactional); - return null; - case "getTargetChannel": - return channel; - case "isTransactional": - return transactional; - case "confirmSelect": + yield null; + } + case "getTargetChannel" -> channel; + case "isTransactional" -> transactional; + case "confirmSelect" -> { confirmSelected.set(true); - return channel.confirmSelect(); - case "isConfirmSelected": - return confirmSelected.get(); - } - return null; + yield channel.confirmSelect(); + } + case "isConfirmSelected" -> confirmSelected.get(); + case "isPublisherConfirms" -> false; + default -> null; + }; }; NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(advice); advisor.addMethodName("close"); @@ -296,14 +351,14 @@ private Channel createProxy(Channel channel, boolean transactional) { advisor.addMethodName("isTransactional"); advisor.addMethodName("confirmSelect"); advisor.addMethodName("isConfirmSelected"); + advisor.addMethodName("isPublisherConfirms"); pf.addAdvisor(advisor); pf.addInterface(ChannelProxy.class); return (Channel) pf.getProxy(); } private void handleClose(Channel channel, boolean transactional) { - - if (transactional && this.txChannels.get() == null ? true : this.channels.get() == null) { + if ((transactional && this.txChannels.get() == null) || (!transactional && this.channels.get() == null)) { physicalClose(channel); } else { @@ -329,7 +384,7 @@ public void closeThreadChannel() { doClose(this.txChannels); } - private void doClose(ThreadLocal channelsTL) { + private void doClose(ThreadLocal<@Nullable Channel> channelsTL) { Channel channel = channelsTL.get(); if (channel != null) { channelsTL.remove(); @@ -364,17 +419,17 @@ Context prepareSwitchContext() { } void switchContext(Context context) { - Channel nonTx = context.getNonTx(); + Channel nonTx = context.nonTx(); if (nonTx != null) { doSwitch(nonTx, this.channels); } - Channel tx = context.getTx(); + Channel tx = context.tx(); if (tx != null) { doSwitch(tx, this.txChannels); } } - private void doSwitch(Channel channel, ThreadLocal channelTL) { + private void doSwitch(Channel channel, ThreadLocal<@Nullable Channel> channelTL) { Channel toClose = channelTL.get(); if (toClose != null) { RabbitUtils.setPhysicalCloseRequired(channel, true); @@ -385,26 +440,7 @@ private void doSwitch(Channel channel, ThreadLocal channelTL) { } - private static class Context { - - private final Channel nonTx; - - private final Channel tx; - - Context(@Nullable Channel nonTx, @Nullable Channel tx) { - this.nonTx = nonTx; - this.tx = tx; - } - - @Nullable - Channel getNonTx() { - return this.nonTx; - } - - @Nullable - Channel getTx() { - return this.tx; - } + private record Context(@Nullable Channel nonTx, @Nullable Channel tx) { } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java new file mode 100644 index 0000000000..568136ad74 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.connection; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriUtils; + +/** + * A {@link NodeLocator} using the Spring WebFlux {@link WebClient}. + * + * @author Gary Russell + * @author Ngoc Nhan + * @since 2.4.8 + * + */ +public class WebFluxNodeLocator implements NodeLocator { + + @Override + public @Nullable Map restCall(WebClient client, String baseUri, String vhost, String queue) + throws URISyntaxException { + + URI uri = new URI(baseUri) + .resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" + queue); + return client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); // NOSONAR magic# + } + + /** + * Create a client instance. + * @param username the username + * @param password the password. + * @return The client. + */ + @Override + public WebClient createClient(String username, String password) { + return WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(username, password)) + .build(); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/package-info.java index de2b6135d5..354c068b0a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes related to connections. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.connection; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/AmqpNackReceivedException.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/AmqpNackReceivedException.java index fb8456e004..34f2a18c74 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/AmqpNackReceivedException.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/AmqpNackReceivedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or 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.amqp.rabbit.core; +import java.io.Serial; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; @@ -29,6 +31,7 @@ */ public class AmqpNackReceivedException extends AmqpException { + @Serial private static final long serialVersionUID = 1L; private final Message failedMessage; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java index 0b19f09519..4a5eb5111d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author 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,10 @@ import java.util.Date; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; @@ -42,11 +46,13 @@ */ public class BatchingRabbitTemplate extends RabbitTemplate { + private final Lock lock = new ReentrantLock(); + private final BatchingStrategy batchingStrategy; private final TaskScheduler scheduler; - private volatile ScheduledFuture scheduledTask; + private volatile @Nullable ScheduledFuture scheduledTask; /** * Create an instance with the supplied parameters. @@ -74,28 +80,33 @@ public BatchingRabbitTemplate(ConnectionFactory connectionFactory, BatchingStrat } @Override - public synchronized void send(String exchange, String routingKey, Message message, CorrelationData correlationData) - throws AmqpException { - - if (correlationData != null) { - if (logger.isDebugEnabled()) { - logger.debug("Cannot use batching with correlation data"); - } - super.send(exchange, routingKey, message, correlationData); - } - else { - if (this.scheduledTask != null) { - this.scheduledTask.cancel(false); + public void send(@Nullable String exchange, @Nullable String routingKey, Message message, + @Nullable CorrelationData correlationData) throws AmqpException { + this.lock.lock(); + try { + if (correlationData != null) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Cannot use batching with correlation data"); + } + super.send(exchange, routingKey, message, correlationData); } - MessageBatch batch = this.batchingStrategy.addToBatch(exchange, routingKey, message); - if (batch != null) { - super.send(batch.getExchange(), batch.getRoutingKey(), batch.getMessage(), null); - } - Date next = this.batchingStrategy.nextRelease(); - if (next != null) { - this.scheduledTask = this.scheduler.schedule((Runnable) () -> releaseBatches(), next); + else { + if (this.scheduledTask != null) { + this.scheduledTask.cancel(false); + } + MessageBatch batch = this.batchingStrategy.addToBatch(exchange, routingKey, message); + if (batch != null) { + super.send(batch.exchange(), batch.routingKey(), batch.message(), null); + } + Date next = this.batchingStrategy.nextRelease(); + if (next != null) { + this.scheduledTask = this.scheduler.schedule(this::releaseBatches, next.toInstant()); + } } } + finally { + this.lock.unlock(); + } } /** @@ -105,9 +116,15 @@ public void flush() { releaseBatches(); } - private synchronized void releaseBatches() { - for (MessageBatch batch : this.batchingStrategy.releaseBatches()) { - super.send(batch.getExchange(), batch.getRoutingKey(), batch.getMessage(), null); + private void releaseBatches() { + this.lock.lock(); + try { + for (MessageBatch batch : this.batchingStrategy.releaseBatches()) { + super.send(batch.exchange(), batch.routingKey(), batch.message(), null); + } + } + finally { + this.lock.unlock(); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEvent.java index b30cdc7a02..761ea35850 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author 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.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.event.AmqpEvent; import org.springframework.util.Assert; /** - * Represents a broker event generated by the Event Exchange Plugin - * (https://www.rabbitmq.com/event-exchange.html). + * Represents a broker event generated by the + * Event Exchange Plugin. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.1 * */ @@ -50,7 +54,7 @@ public BrokerEvent(Object source, MessageProperties properties) { * The event type ({@link MessageProperties#getReceivedRoutingKey()}). * @return the type. */ - public String getEventType() { + public @Nullable String getEventType() { return this.properties.getReceivedRoutingKey(); } @@ -58,7 +62,7 @@ public String getEventType() { * Properties of the event {@link MessageProperties#getHeaders()}. * @return the properties. */ - public Map getEventProperties() { + public Map getEventProperties() { return this.properties.getHeaders(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java index 3b453b7e74..0436ce5c87 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author 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,12 @@ package org.springframework.amqp.rabbit.core; import java.util.Arrays; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.core.Base64UrlNamingStrategy; @@ -37,19 +40,20 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.SmartLifecycle; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * When the event-exchange-plugin is enabled (see - * https://www.rabbitmq.com/event-exchange.html), if an object of this type is declared as - * a bean, selected events will be published as {@link BrokerEvent}s. Such events can then - * be consumed using an {@code ApplicationListener} or {@code @EventListener} method. + * When the Event Exchange Plugin is enabled, + * if an object of this type is declared as a bean, selected events will be published as {@link BrokerEvent}s. + * Such events can then be consumed using an {@code ApplicationListener} or {@code @EventListener} method. * An {@link AnonymousQueue} will be bound to the {@code amq.rabbitmq.event} topic exchange * with the supplied keys. * * @author Gary Russell + * @author Christian Tzolov + * @author Artem Bilan + * * @since 2.1 * */ @@ -58,6 +62,8 @@ public class BrokerEventListener implements MessageListener, ApplicationEventPub private static final Log logger = LogFactory.getLog(BrokerEventListener.class); // NOSONAR - lower case + private final Lock lock = new ReentrantLock(); + private final AbstractMessageListenerContainer container; private final String[] eventKeys; @@ -76,9 +82,9 @@ public class BrokerEventListener implements MessageListener, ApplicationEventPub private boolean stopInvoked; - private Exception bindingsFailedException; + private @Nullable Exception bindingsFailedException; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; /** * Construct an instance using the supplied connection factory and event keys. Event @@ -91,6 +97,7 @@ public class BrokerEventListener implements MessageListener, ApplicationEventPub * @param connectionFactory the connection factory. * @param eventKeys the event keys. */ + @SuppressWarnings("this-escape") public BrokerEventListener(ConnectionFactory connectionFactory, String... eventKeys) { this(new DirectMessageListenerContainer(connectionFactory), true, eventKeys); } @@ -111,6 +118,7 @@ public BrokerEventListener(AbstractMessageListenerContainer container, String... this(container, false, eventKeys); } + @SuppressWarnings("this-escape") private BrokerEventListener(AbstractMessageListenerContainer container, boolean ownContainer, String... eventKeys) { Assert.notNull(container, "listener container cannot be null"); Assert.isTrue(!ObjectUtils.isEmpty(eventKeys), "At least one event key is required"); @@ -137,34 +145,52 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv } @Override - public synchronized void start() { - if (!this.running) { - if (this.stopInvoked) { - // redeclare auto-delete queue - this.stopInvoked = false; - onCreate(null); - } - if (this.ownContainer) { - this.container.start(); + public void start() { + this.lock.lock(); + try { + if (!this.running) { + if (this.stopInvoked) { + // redeclare auto-delete queue + this.stopInvoked = false; + onCreate(null); + } + if (this.ownContainer) { + this.container.start(); + } + this.running = true; } - this.running = true; + } + finally { + this.lock.unlock(); } } @Override - public synchronized void stop() { - if (this.running) { - if (this.ownContainer) { - this.container.stop(); + public void stop() { + this.lock.lock(); + try { + if (this.running) { + if (this.ownContainer) { + this.container.stop(); + } + this.running = false; + this.stopInvoked = true; } - this.running = false; - this.stopInvoked = true; + } + finally { + this.lock.unlock(); } } @Override - public synchronized boolean isRunning() { - return this.running; + public boolean isRunning() { + this.lock.lock(); + try { + return this.running; + } + finally { + this.lock.unlock(); + } } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java index 4702175c57..7a8d14868e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,8 @@ package org.springframework.amqp.rabbit.core; -import org.springframework.lang.Nullable; - import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; /** * Basic callback for use in RabbitTemplate. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/CorrelationDataPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/CorrelationDataPostProcessor.java index 0b5d0456f2..d3073e75ee 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/CorrelationDataPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/CorrelationDataPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 the original author or 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.amqp.rabbit.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.connection.CorrelationData; @@ -25,6 +27,8 @@ * {@link org.springframework.amqp.core.MessagePostProcessor}s. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6.7 * */ @@ -37,6 +41,6 @@ public interface CorrelationDataPostProcessor { * @param correlationData the existing data (if present). * @return the correlation data. */ - CorrelationData postProcess(Message message, CorrelationData correlationData); + CorrelationData postProcess(Message message, @Nullable CorrelationData correlationData); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclarationExceptionEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclarationExceptionEvent.java index e07abfe010..9ed783a6aa 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclarationExceptionEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclarationExceptionEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,11 @@ package org.springframework.amqp.rabbit.core; +import java.io.Serial; + +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Declarable; -import org.springframework.lang.Nullable; /** * Application event published when a declaration exception occurs. @@ -28,9 +31,10 @@ */ public class DeclarationExceptionEvent extends RabbitAdminEvent { + @Serial private static final long serialVersionUID = -8367796410619780665L; - private final transient Declarable declarable; + private final transient @Nullable Declarable declarable; private final Throwable throwable; @@ -43,8 +47,7 @@ public DeclarationExceptionEvent(Object source, @Nullable Declarable declarable, /** * @return the declarable - if null, we were declaring a broker-named queue. */ - @Nullable - public Declarable getDeclarable() { + public @Nullable Declarable getDeclarable() { return this.declarable; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclareExchangeConnectionListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclareExchangeConnectionListener.java index f814835cc7..58fabc24f6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclareExchangeConnectionListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclareExchangeConnectionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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.amqp.rabbit.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Exchange; import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionListener; @@ -25,6 +27,8 @@ * connection is established. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.5.4 * */ @@ -40,16 +44,13 @@ public DeclareExchangeConnectionListener(Exchange exchange, RabbitAdmin admin) { } @Override - public void onCreate(Connection connection) { + public void onCreate(@Nullable Connection connection) { try { this.admin.declareExchange(this.exchange); } catch (Exception e) { + // Ignoire } } - @Override - public void onClose(Connection connection) { - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index fa77077b97..1831399049 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,18 +19,27 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import com.rabbitmq.client.AMQP.Queue.DeclareOk; +import com.rabbitmq.client.AMQP.Queue.PurgeOk; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AmqpAdmin; @@ -55,17 +64,12 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.lang.Nullable; import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.rabbitmq.client.AMQP.Queue.DeclareOk; -import com.rabbitmq.client.AMQP.Queue.PurgeOk; -import com.rabbitmq.client.Channel; - /** * RabbitMQ implementation of portable AMQP administrative operations for AMQP >= 0.9.1. * @@ -75,6 +79,8 @@ * @author Ed Scriven * @author Gary Russell * @author Artem Bilan + * @author Christian Tzolov + * @author Ngoc Nhan */ @ManagedResource(description = "Admin Tasks") public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, ApplicationEventPublisherAware, @@ -113,38 +119,47 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat */ public static final Object QUEUE_CONSUMER_COUNT = "QUEUE_CONSUMER_COUNT"; - private static final String DELAYED_MESSAGE_EXCHANGE = "x-delayed-message"; + public static final String DELAYED_MESSAGE_EXCHANGE = "x-delayed-message"; /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR + private final Lock lock = new ReentrantLock(); + private final RabbitTemplate rabbitTemplate; - private final Object lifecycleMonitor = new Object(); + private final Lock lifecycleLock = new ReentrantLock(); private final ConnectionFactory connectionFactory; + private final Set manualDeclarables = Collections.synchronizedSet(new LinkedHashSet<>()); + + private final Lock manualDeclarablesLock = new ReentrantLock(); + + @SuppressWarnings("NullAway.Init") private String beanName; - private RetryTemplate retryTemplate; + private @Nullable RetryTemplate retryTemplate; private boolean retryDisabled; private boolean autoStartup = true; - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; private boolean ignoreDeclarationExceptions; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); private boolean explicitDeclarationsOnly; + private boolean redeclareManualDeclarations; + private volatile boolean running = false; - private volatile DeclarationExceptionEvent lastDeclarationExceptionEvent; + private volatile @Nullable DeclarationExceptionEvent lastDeclarationExceptionEvent; /** * Construct an instance using the provided {@link ConnectionFactory}. @@ -191,10 +206,9 @@ public void setIgnoreDeclarationExceptions(boolean ignoreDeclarationExceptions) /** * @return the last {@link DeclarationExceptionEvent} that was detected in this admin. - * * @since 1.6 */ - public DeclarationExceptionEvent getLastDeclarationExceptionEvent() { + public @Nullable DeclarationExceptionEvent getLastDeclarationExceptionEvent() { return this.lastDeclarationExceptionEvent; } @@ -220,6 +234,9 @@ public void declareExchange(final Exchange exchange) { try { this.rabbitTemplate.execute(channel -> { declareExchanges(channel, exchange); + if (this.redeclareManualDeclarations) { + this.manualDeclarables.add(exchange); + } return null; }); } @@ -230,14 +247,16 @@ public void declareExchange(final Exchange exchange) { @Override @ManagedOperation(description = "Delete an exchange from the broker") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public boolean deleteExchange(final String exchangeName) { - return this.rabbitTemplate.execute(channel -> { // NOSONAR never returns null + return this.rabbitTemplate.execute(channel -> { if (isDeletingDefaultExchange(exchangeName)) { return true; } try { channel.exchangeDelete(exchangeName); + removeExchangeBindings(exchangeName); } catch (@SuppressWarnings(UNUSED) IOException e) { return false; @@ -246,6 +265,24 @@ public boolean deleteExchange(final String exchangeName) { }); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private void removeExchangeBindings(final String exchangeName) { + this.manualDeclarablesLock.lock(); + try { + this.manualDeclarables.stream() + .filter(dec -> dec instanceof Exchange ex && ex.getName().equals(exchangeName)) + .collect(Collectors.toSet()) + .forEach(this.manualDeclarables::remove); + this.manualDeclarables.removeIf(next -> + next instanceof Binding binding + && ((!binding.isDestinationQueue() && binding.getDestination().equals(exchangeName)) + || binding.getExchange().equals(exchangeName))); + } + finally { + this.manualDeclarablesLock.unlock(); + } + } + // Queue operations /** @@ -259,14 +296,16 @@ public boolean deleteExchange(final String exchangeName) { * true. */ @Override - @ManagedOperation(description = - "Declare a queue on the broker (this operation is not available remotely)") - @Nullable - public String declareQueue(final Queue queue) { + @ManagedOperation(description = "Declare a queue on the broker (this operation is not available remotely)") + public @Nullable String declareQueue(final Queue queue) { try { return this.rabbitTemplate.execute(channel -> { DeclareOk[] declared = declareQueues(channel, queue); - return declared.length > 0 ? declared[0].getQueue() : null; + String result = declared.length > 0 ? declared[0].getQueue() : null; + if (this.redeclareManualDeclarations) { + this.manualDeclarables.add(queue); + } + return result; }); } catch (AmqpException e) { @@ -285,8 +324,8 @@ public String declareQueue(final Queue queue) { @Override @ManagedOperation(description = "Declare a queue with a broker-generated name (this operation is not available remotely)") - @Nullable - public Queue declareQueue() { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public @Nullable Queue declareQueue() { try { DeclareOk declareOk = this.rabbitTemplate.execute(Channel::queueDeclare); return new Queue(declareOk.getQueue(), false, true, true); // NOSONAR never null @@ -299,10 +338,12 @@ public Queue declareQueue() { @Override @ManagedOperation(description = "Delete a queue from the broker") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public boolean deleteQueue(final String queueName) { - return this.rabbitTemplate.execute(channel -> { // NOSONAR never returns null + return this.rabbitTemplate.execute(channel -> { try { channel.queueDelete(queueName); + removeQueueBindings(queueName); } catch (@SuppressWarnings(UNUSED) IOException e) { return false; @@ -317,10 +358,29 @@ public boolean deleteQueue(final String queueName) { public void deleteQueue(final String queueName, final boolean unused, final boolean empty) { this.rabbitTemplate.execute(channel -> { channel.queueDelete(queueName, unused, empty); + removeQueueBindings(queueName); return null; }); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private void removeQueueBindings(final String queueName) { + this.manualDeclarablesLock.lock(); + try { + this.manualDeclarables.stream() + .filter(dec -> dec instanceof Queue queue && queue.getName().equals(queueName)) + .collect(Collectors.toSet()) + .forEach(this.manualDeclarables::remove); + this.manualDeclarables.removeIf(next -> + next instanceof Binding binding + && (binding.isDestinationQueue() + && binding.getDestination().equals(queueName))); + } + finally { + this.manualDeclarablesLock.unlock(); + } + } + @Override @ManagedOperation(description = "Purge a queue and optionally don't wait for the purge to occur") public void purgeQueue(final String queueName, final boolean noWait) { @@ -334,6 +394,7 @@ public void purgeQueue(final String queueName, final boolean noWait) { @Override @ManagedOperation(description = "Purge a queue and return the number of messages purged") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public int purgeQueue(final String queueName) { return this.rabbitTemplate.execute(channel -> { // NOSONAR never returns null PurgeOk queuePurged = channel.queuePurge(queueName); @@ -346,12 +407,14 @@ public int purgeQueue(final String queueName) { // Binding @Override - @ManagedOperation(description = - "Declare a binding on the broker (this operation is not available remotely)") + @ManagedOperation(description = "Declare a binding on the broker (this operation is not available remotely)") public void declareBinding(final Binding binding) { try { this.rabbitTemplate.execute(channel -> { declareBindings(channel, binding); + if (this.redeclareManualDeclarations) { + this.manualDeclarables.add(binding); + } return null; }); } @@ -361,8 +424,7 @@ public void declareBinding(final Binding binding) { } @Override - @ManagedOperation(description = - "Remove a binding from the broker (this operation is not available remotely)") + @ManagedOperation(description = "Remove a binding from the broker (this operation is not available remotely)") public void removeBinding(final Binding binding) { this.rabbitTemplate.execute(channel -> { if (binding.isDestinationQueue()) { @@ -377,6 +439,7 @@ public void removeBinding(final Binding binding) { channel.exchangeUnbind(binding.getDestination(), binding.getExchange(), binding.getRoutingKey(), binding.getArguments()); } + this.manualDeclarables.remove(binding); return null; }); } @@ -387,7 +450,7 @@ public void removeBinding(final Binding binding) { */ @Override @ManagedOperation(description = "Get queue name, message count and consumer count") - public Properties getQueueProperties(final String queueName) { + public @Nullable Properties getQueueProperties(final String queueName) { QueueInformation queueInfo = getQueueInfo(queueName); if (queueInfo != null) { Properties props = new Properties(); @@ -402,7 +465,7 @@ public Properties getQueueProperties(final String queueName) { } @Override - public QueueInformation getQueueInfo(String queueName) { + public @Nullable QueueInformation getQueueInfo(String queueName) { Assert.hasText(queueName, "'queueName' cannot be null or empty"); return this.rabbitTemplate.execute(channel -> { try { @@ -416,11 +479,12 @@ public QueueInformation getQueueInfo(String queueName) { e); } try { - if (channel instanceof ChannelProxy) { - ((ChannelProxy) channel).getTargetChannel().close(); + if (channel instanceof ChannelProxy proxy) { + proxy.getTargetChannel().close(); } } catch (@SuppressWarnings(UNUSED) TimeoutException e1) { + // Ignore } return null; } @@ -444,6 +508,37 @@ public void setExplicitDeclarationsOnly(boolean explicitDeclarationsOnly) { this.explicitDeclarationsOnly = explicitDeclarationsOnly; } + /** + * Normally, when a connection is recovered, the admin only recovers auto-delete queues, + * etc., that are declared as beans in the application context. When this is true, it + * will also redeclare any manually declared {@link Declarable}s via admin methods. + * @return true to redeclare. + * @since 2.4 + */ + public boolean isRedeclareManualDeclarations() { + return this.redeclareManualDeclarations; + } + + /** + * Normally, when a connection is recovered, the admin only recovers auto-delete + * queues, etc., that are declared as beans in the application context. When this is + * true, it will also redeclare any manually declared {@link Declarable}s via admin + * methods. When a queue or exchange is deleted, it will no longer be recovered, nor + * will any corresponding bindings. + * @param redeclareManualDeclarations true to redeclare. + * @since 2.4 + * @see #declareQueue(Queue) + * @see #declareExchange(Exchange) + * @see #declareBinding(Binding) + * @see #deleteQueue(String) + * @see #deleteExchange(String) + * @see #removeBinding(Binding) + * @see #resetAllManualDeclarations() + */ + public void setRedeclareManualDeclarations(boolean redeclareManualDeclarations) { + this.redeclareManualDeclarations = redeclareManualDeclarations; + } + /** * Set a retry template for auto declarations. There is a race condition with * auto-delete, exclusive queues in that the queue might still exist for a short time, @@ -489,8 +584,8 @@ public boolean isAutoStartup() { */ @Override public void afterPropertiesSet() { - - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { if (this.running || !this.autoStartup) { return; @@ -505,8 +600,8 @@ public void afterPropertiesSet() { backOffPolicy.setMaxInterval(DECLARE_MAX_RETRY_INTERVAL); this.retryTemplate.setBackOffPolicy(backOffPolicy); } - if (this.connectionFactory instanceof CachingConnectionFactory && - ((CachingConnectionFactory) this.connectionFactory).getCacheMode() == CacheMode.CONNECTION) { + if (this.connectionFactory instanceof CachingConnectionFactory ccf && + ccf.getCacheMode() == CacheMode.CONNECTION) { this.logger.warn("RabbitAdmin auto declaration is not supported with CacheMode.CONNECTION"); return; } @@ -524,7 +619,7 @@ public void afterPropertiesSet() { /* * ...but it is possible for this to happen twice in the same ConnectionFactory (if more than * one concurrent Connection is allowed). It's idempotent, so no big deal (a bit of network - * chatter). In fact it might even be a good thing: exclusive queues only make sense if they are + * chatter). In fact, it might even be a good thing: exclusive queues only make sense if they are * declared for every connection. If anyone has a problem with it: use auto-startup="false". */ if (this.retryTemplate != null) { @@ -544,7 +639,9 @@ public void afterPropertiesSet() { }); this.running = true; - + } + finally { + this.lifecycleLock.unlock(); } } @@ -554,18 +651,25 @@ public void afterPropertiesSet() { */ @Override // NOSONAR complexity public void initialize() { + redeclareBeanDeclarables(); + redeclareManualDeclarables(); + } + /** + * Process bean declarables. + */ + private void redeclareBeanDeclarables() { if (this.applicationContext == null) { this.logger.debug("no ApplicationContext has been set, cannot auto-declare Exchanges, Queues, and Bindings"); return; } this.logger.debug("Initializing declarations"); - Collection contextExchanges = new LinkedList( + Collection contextExchanges = new LinkedList<>( this.applicationContext.getBeansOfType(Exchange.class).values()); - Collection contextQueues = new LinkedList( + Collection contextQueues = new LinkedList<>( this.applicationContext.getBeansOfType(Queue.class).values()); - Collection contextBindings = new LinkedList( + Collection contextBindings = new LinkedList<>( this.applicationContext.getBeansOfType(Binding.class).values()); Collection customizers = this.applicationContext.getBeansOfType(DeclarableCustomizer.class).values(); @@ -577,7 +681,7 @@ public void initialize() { final Collection bindings = filterDeclarables(contextBindings, customizers); for (Exchange exchange : exchanges) { - if ((!exchange.isDurable() || exchange.isAutoDelete()) && this.logger.isInfoEnabled()) { + if ((!exchange.isDurable() || exchange.isAutoDelete()) && this.logger.isInfoEnabled()) { this.logger.info("Auto-declaring a non-durable or auto-delete Exchange (" + exchange.getName() + ") durable:" + exchange.isDurable() + ", auto-delete:" + exchange.isAutoDelete() + ". " @@ -597,35 +701,78 @@ public void initialize() { } } - if (exchanges.size() == 0 && queues.size() == 0 && bindings.size() == 0) { + if (exchanges.isEmpty() && queues.isEmpty() && bindings.isEmpty() && this.manualDeclarables.isEmpty()) { this.logger.debug("Nothing to declare"); return; } this.rabbitTemplate.execute(channel -> { - declareExchanges(channel, exchanges.toArray(new Exchange[exchanges.size()])); - declareQueues(channel, queues.toArray(new Queue[queues.size()])); - declareBindings(channel, bindings.toArray(new Binding[bindings.size()])); + declareExchanges(channel, exchanges.toArray(new Exchange[0])); + declareQueues(channel, queues.toArray(new Queue[0])); + declareBindings(channel, bindings.toArray(new Binding[0])); return null; }); this.logger.debug("Declarations finished"); } + /** + * Process manual declarables. + */ + private void redeclareManualDeclarables() { + if (!this.manualDeclarables.isEmpty()) { + this.manualDeclarablesLock.lock(); + try { + this.logger.debug("Redeclaring manually declared Declarables"); + for (Declarable dec : this.manualDeclarables) { + if (dec instanceof Queue queue) { + declareQueue(queue); + } + else if (dec instanceof Exchange exch) { + declareExchange(exch); + } + else { + declareBinding((Binding) dec); + } + } + } + finally { + this.manualDeclarablesLock.unlock(); + } + } + + } + + /** + * Invoke this method to prevent the admin from recovering any declarations made + * by calls to {@code declare*()} methods. + * @since 2.4 + * @see #setRedeclareManualDeclarations(boolean) + */ + public void resetAllManualDeclarations() { + this.manualDeclarables.clear(); + } + + @Override + public Set getManualDeclarableSet() { + return Collections.unmodifiableSet(this.manualDeclarables); + } + private void processDeclarables(Collection contextExchanges, Collection contextQueues, Collection contextBindings) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation Collection declarables = this.applicationContext.getBeansOfType(Declarables.class, false, true) .values(); declarables.forEach(d -> { d.getDeclarables().forEach(declarable -> { - if (declarable instanceof Exchange) { - contextExchanges.add((Exchange) declarable); + if (declarable instanceof Exchange exch) { + contextExchanges.add(exch); } - else if (declarable instanceof Queue) { - contextQueues.add((Queue) declarable); + else if (declarable instanceof Queue queue) { + contextQueues.add(queue); } - else if (declarable instanceof Binding) { - contextBindings.add((Binding) declarable); + else if (declarable instanceof Binding binding) { + contextBindings.add(binding); } }); }); @@ -639,7 +786,7 @@ else if (declarable instanceof Binding) { * @return a new collection containing {@link Declarable}s that should be declared by this * admin. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "NullAway" }) // Dataflow analysis limitation private Collection filterDeclarables(Collection declarables, Collection customizers) { @@ -653,13 +800,13 @@ private Collection filterDeclarables(Collection dec customizers.forEach(cust -> ref.set((T) cust.apply(ref.get()))); return ref.get(); }) - .collect(Collectors.toList()); + .toList(); } private boolean declarableByMe(T dec) { return (dec.getDeclaringAdmins().isEmpty() && !this.explicitDeclarationsOnly) // NOSONAR boolean complexity || dec.getDeclaringAdmins().contains(this) - || (this.beanName != null && dec.getDeclaringAdmins().contains(this.beanName)); + || dec.getDeclaringAdmins().contains(this.beanName); } // private methods for declaring Exchanges, Queues, and Bindings on a Channel @@ -675,10 +822,10 @@ private void declareExchanges(final Channel channel, final Exchange... exchanges if (exchange.isDelayed()) { Map arguments = exchange.getArguments(); if (arguments == null) { - arguments = new HashMap(); + arguments = new HashMap<>(); } else { - arguments = new HashMap(arguments); + arguments = new HashMap<>(arguments); } arguments.put("x-delayed-type", exchange.getType()); channel.exchangeDeclare(exchange.getName(), DELAYED_MESSAGE_EXCHANGE, exchange.isDurable(), @@ -697,9 +844,8 @@ private void declareExchanges(final Channel channel, final Exchange... exchanges } private DeclareOk[] declareQueues(final Channel channel, final Queue... queues) throws IOException { - List declareOks = new ArrayList(queues.length); - for (int i = 0; i < queues.length; i++) { - Queue queue = queues[i]; + List declareOks = new ArrayList<>(queues.length); + for (Queue queue : queues) { if (!queue.getName().startsWith("amq.")) { if (this.logger.isDebugEnabled()) { this.logger.debug("declaring Queue '" + queue.getName() + "'"); @@ -726,7 +872,7 @@ else if (this.logger.isDebugEnabled()) { this.logger.debug(queue.getName() + ": Queue with name that starts with 'amq.' cannot be declared."); } } - return declareOks.toArray(new DeclareOk[declareOks.size()]); + return declareOks.toArray(new DeclareOk[0]); } private void closeChannelAfterIllegalArg(final Channel channel, Queue queue) { @@ -734,8 +880,8 @@ private void closeChannelAfterIllegalArg(final Channel channel, Queue queue) { this.logger.error("Exception while declaring queue: '" + queue.getName() + "'"); } try { - if (channel instanceof ChannelProxy) { - ((ChannelProxy) channel).getTargetChannel().close(); + if (channel instanceof ChannelProxy proxy) { + proxy.getTargetChannel().close(); } } catch (IOException | TimeoutException e1) { @@ -839,6 +985,7 @@ private boolean isRemovingImplicitQueueBinding(Binding binding) { return false; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private boolean isImplicitQueueBinding(Binding binding) { return isDefaultExchange(binding.getExchange()) && binding.getDestination().equals(binding.getRoutingKey()); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdminEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdminEvent.java index d7d50fb16d..6ba0913ea2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdminEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdminEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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.amqp.rabbit.core; +import java.io.Serial; + import org.springframework.amqp.event.AmqpEvent; /** @@ -27,6 +29,7 @@ */ public class RabbitAdminEvent extends AmqpEvent { + @Serial private static final long serialVersionUID = 1L; public RabbitAdminEvent(Object source) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupport.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupport.java index 406520a82e..f81ecfc37f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupport.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** * Convenient super class for application classes that need RabbitMQ access. @@ -45,7 +45,7 @@ public class RabbitGatewaySupport implements InitializingBean { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR - private RabbitOperations rabbitOperations; + private @Nullable RabbitOperations rabbitOperations; /** * Set the Rabbit connection factory to be used by the gateway. @@ -73,8 +73,7 @@ protected RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactor /** * @return The Rabbit ConnectionFactory used by the gateway. */ - @Nullable - public final ConnectionFactory getConnectionFactory() { + public final @Nullable ConnectionFactory getConnectionFactory() { return (this.rabbitOperations != null ? this.rabbitOperations.getConnectionFactory() : null); } @@ -90,7 +89,7 @@ public final void setRabbitOperations(RabbitOperations rabbitOperations) { /** * @return The {@link RabbitOperations} for the gateway. */ - public final RabbitOperations getRabbitOperations() { + public final @Nullable RabbitOperations getRabbitOperations() { return this.rabbitOperations; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessageOperations.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessageOperations.java index 5b62f7a42c..8b3d9baa90 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessageOperations.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessageOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.core.MessagePostProcessor; @@ -100,8 +102,8 @@ void convertAndSend(String exchange, String routingKey, Object payload, MessageP * @param postProcessor the post processor to apply to the message * @throws MessagingException a messaging exception. */ - void convertAndSend(String exchange, String routingKey, Object payload, Map headers, MessagePostProcessor postProcessor) throws MessagingException; + void convertAndSend(String exchange, String routingKey, Object payload, + @Nullable Map headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException; /** * Send a request message to a specific exchange with a specific routing key and @@ -113,6 +115,7 @@ void convertAndSend(String exchange, String routingKey, Object payload, Map sendAndReceive(String exchange, String routingKey, Message requestMessage) throws MessagingException; /** @@ -129,7 +132,7 @@ void convertAndSend(String exchange, String routingKey, Object payload, Map T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass) + @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass) throws MessagingException; /** @@ -148,8 +151,8 @@ T convertSendAndReceive(String exchange, String routingKey, Object request, * could not be received, for example due to a timeout * @throws MessagingException a messaging exception. */ - T convertSendAndReceive(String exchange, String routingKey, Object request, Map headers, - Class targetClass) throws MessagingException; + @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, + @Nullable Map headers, Class targetClass) throws MessagingException; /** * Convert the given request Object to serialized form, possibly using a @@ -167,15 +170,15 @@ T convertSendAndReceive(String exchange, String routingKey, Object request, * could not be received, for example due to a timeout * @throws MessagingException a messaging exception. */ - T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass, - MessagePostProcessor requestPostProcessor) throws MessagingException; + @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass, + @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException; /** * Convert the given request Object to serialized form, possibly using a * {@link org.springframework.messaging.converter.MessageConverter}, * wrap it as a message with the given headers, apply the given post processor * and send the resulting {@link Message} to a specific exchange with a - * specific routing key,, receive the reply and convert its body of the + * specific routing key, receive the reply and convert its body of the * given target class. * @param exchange the name of the exchange * @param routingKey the routing key @@ -188,7 +191,8 @@ T convertSendAndReceive(String exchange, String routingKey, Object request, * could not be received, for example due to a timeout * @throws MessagingException a messaging exception. */ - T convertSendAndReceive(String exchange, String routingKey, Object request, Map headers, - Class targetClass, MessagePostProcessor requestPostProcessor) throws MessagingException; + @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, + @Nullable Map headers, Class targetClass, + @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java index e323cd5bda..4cc3330847 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,14 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.MessagingMessageConverter; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.converter.MessageConversionException; @@ -42,12 +43,14 @@ public class RabbitMessagingTemplate extends AbstractMessagingTemplate implements RabbitMessageOperations, InitializingBean { + @SuppressWarnings("NullAway.Init") private RabbitTemplate rabbitTemplate; private MessageConverter amqpMessageConverter = new MessagingMessageConverter(); private boolean converterSet; + private boolean useTemplateDefaultReceiveQueue; /** * Constructor for use with bean properties. @@ -65,12 +68,12 @@ public RabbitMessagingTemplate(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; } - /** * Set the {@link RabbitTemplate} to use. * @param rabbitTemplate the template. */ public void setRabbitTemplate(RabbitTemplate rabbitTemplate) { + Assert.notNull(rabbitTemplate, "'rabbitTemplate' must not be null"); this.rabbitTemplate = rabbitTemplate; } @@ -107,11 +110,23 @@ public MessageConverter getAmqpMessageConverter() { return this.amqpMessageConverter; } + /** + * When true, use the underlying {@link RabbitTemplate}'s defaultReceiveQueue property + * (if configured) for receive only methods instead of the {@code defaultDestination} + * configured in this template. Set this to true to use the template's queue instead. + * Default false, but will be true in a future release. + * @param useTemplateDefaultReceiveQueue true to use the template's queue. + * @since 2.2.22 + */ + public void setUseTemplateDefaultReceiveQueue(boolean useTemplateDefaultReceiveQueue) { + this.useTemplateDefaultReceiveQueue = useTemplateDefaultReceiveQueue; + } + @Override public void afterPropertiesSet() { Assert.notNull(getRabbitTemplate(), "Property 'rabbitTemplate' is required"); Assert.notNull(getAmqpMessageConverter(), "Property 'amqpMessageConverter' is required"); - if (!this.converterSet && this.rabbitTemplate.getMessageConverter() != null) { + if (!this.converterSet) { ((MessagingMessageConverter) this.amqpMessageConverter) .setPayloadConverter(this.rabbitTemplate.getMessageConverter()); } @@ -144,39 +159,35 @@ public void convertAndSend(String exchange, String routingKey, Object payload, @Override public void convertAndSend(String exchange, String routingKey, Object payload, @Nullable Map headers, @Nullable MessagePostProcessor postProcessor) - throws MessagingException { + throws MessagingException { Message message = doConvert(payload, headers, postProcessor); send(exchange, routingKey, message); } @Override - @Nullable - public Message sendAndReceive(String exchange, String routingKey, Message requestMessage) + public @Nullable Message sendAndReceive(String exchange, String routingKey, Message requestMessage) throws MessagingException { return doSendAndReceive(exchange, routingKey, requestMessage); } @Override - @Nullable - public T convertSendAndReceive(String exchange, String routingKey, Object request, + public @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass) throws MessagingException { return convertSendAndReceive(exchange, routingKey, request, null, targetClass); } @Override - @Nullable - public T convertSendAndReceive(String exchange, String routingKey, Object request, + public @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, @Nullable Map headers, Class targetClass) throws MessagingException { return convertSendAndReceive(exchange, routingKey, request, headers, targetClass, null); } @Override - @Nullable - public T convertSendAndReceive(String exchange, String routingKey, Object request, + public @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass, @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException { return convertSendAndReceive(exchange, routingKey, request, null, targetClass, requestPostProcessor); @@ -184,8 +195,7 @@ public T convertSendAndReceive(String exchange, String routingKey, Object re @SuppressWarnings("unchecked") @Override - @Nullable - public T convertSendAndReceive(String exchange, String routingKey, Object request, + public @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, @Nullable Map headers, Class targetClass, @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException { @@ -198,8 +208,8 @@ public T convertSendAndReceive(String exchange, String routingKey, Object re protected void doSend(String destination, Message message) { try { Object correlation = message.getHeaders().get(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION); - if (correlation instanceof CorrelationData) { - this.rabbitTemplate.send(destination, createMessage(message), (CorrelationData) correlation); + if (correlation instanceof CorrelationData corrData) { + this.rabbitTemplate.send(destination, createMessage(message), corrData); } else { this.rabbitTemplate.send(destination, createMessage(message)); @@ -213,8 +223,8 @@ protected void doSend(String destination, Message message) { protected void doSend(String exchange, String routingKey, Message message) { try { Object correlation = message.getHeaders().get(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION); - if (correlation instanceof CorrelationData) { - this.rabbitTemplate.send(exchange, routingKey, createMessage(message), (CorrelationData) correlation); + if (correlation instanceof CorrelationData corrData) { + this.rabbitTemplate.send(exchange, routingKey, createMessage(message), corrData); } else { this.rabbitTemplate.send(exchange, routingKey, createMessage(message)); @@ -225,9 +235,29 @@ protected void doSend(String exchange, String routingKey, Message message) { } } + @Override + public @Nullable Message receive() { + return doReceive(resolveDestination()); + } + + @Override + public @Nullable T receiveAndConvert(Class targetClass) { + return receiveAndConvert(resolveDestination(), targetClass); + } + + private String resolveDestination() { + String dest = null; + if (this.useTemplateDefaultReceiveQueue) { + dest = this.rabbitTemplate.getDefaultReceiveQueue(); + } + if (dest == null) { + dest = getRequiredDefaultDestination(); + } + return dest; + } @Override - protected Message doReceive(String destination) { + protected @Nullable Message doReceive(String destination) { try { org.springframework.amqp.core.Message amqpMessage = this.rabbitTemplate.receive(destination); return convertAmqpMessage(amqpMessage); @@ -237,10 +267,8 @@ protected Message doReceive(String destination) { } } - @Override - @Nullable - protected Message doSendAndReceive(String destination, Message requestMessage) { + protected @Nullable Message doSendAndReceive(String destination, Message requestMessage) { try { org.springframework.amqp.core.Message amqpMessage = this.rabbitTemplate.sendAndReceive( destination, createMessage(requestMessage)); @@ -251,8 +279,7 @@ protected Message doSendAndReceive(String destination, Message requestMess } } - @Nullable - protected Message doSendAndReceive(String exchange, String routingKey, Message requestMessage) { + protected @Nullable Message doSendAndReceive(String exchange, String routingKey, Message requestMessage) { try { org.springframework.amqp.core.Message amqpMessage = this.rabbitTemplate.sendAndReceive( exchange, routingKey, createMessage(requestMessage)); @@ -272,8 +299,7 @@ private org.springframework.amqp.core.Message createMessage(Message message) } } - @Nullable - protected Message convertAmqpMessage(@Nullable org.springframework.amqp.core.Message message) { + protected @Nullable Message convertAmqpMessage(org.springframework.amqp.core.@Nullable Message message) { if (message == null) { return null; } @@ -287,8 +313,8 @@ protected Message convertAmqpMessage(@Nullable org.springframework.amqp.core. @SuppressWarnings("ThrowableResultOfMethodCallIgnored") protected MessagingException convertAmqpException(RuntimeException ex) { - if (ex instanceof MessagingException) { - return (MessagingException) ex; + if (ex instanceof MessagingException mex) { + return mex; } if (ex instanceof org.springframework.amqp.support.converter.MessageConversionException) { return new MessageConversionException(ex.getMessage(), ex); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitOperations.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitOperations.java index 0e895726f7..248e4306df 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitOperations.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; @@ -24,7 +26,6 @@ import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.context.Lifecycle; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; /** * Rabbit specific methods for Amqp functionality. @@ -37,19 +38,18 @@ public interface RabbitOperations extends AmqpTemplate, Lifecycle { /** - * Execute the callback with a channel and reliably close the channel afterwards. + * Execute the callback with a channel and reliably close the channel afterward. * @param action the call back. * @param the return type. * @return the result from the * {@link ChannelCallback#doInRabbit(com.rabbitmq.client.Channel)}. * @throws AmqpException if one occurs. */ - @Nullable - T execute(ChannelCallback action) throws AmqpException; + @Nullable T execute(ChannelCallback action) throws AmqpException; /** * Invoke the callback and run all operations on the template argument in a dedicated - * thread-bound channel and reliably close the channel afterwards. + * thread-bound channel and reliably close the channel afterward. * @param action the call back. * @param the return type. * @return the result from the @@ -57,8 +57,7 @@ public interface RabbitOperations extends AmqpTemplate, Lifecycle { * @throws AmqpException if one occurs. * @since 2.0 */ - @Nullable - default T invoke(OperationsCallback action) throws AmqpException { + default @Nullable T invoke(OperationsCallback action) throws AmqpException { return invoke(action, null, null); } @@ -72,9 +71,8 @@ default T invoke(OperationsCallback action) throws AmqpException { * @return the result of the action method. * @since 2.1 */ - @Nullable - T invoke(OperationsCallback action, @Nullable com.rabbitmq.client.ConfirmCallback acks, - @Nullable com.rabbitmq.client.ConfirmCallback nacks); + @Nullable T invoke(OperationsCallback action, com.rabbitmq.client.@Nullable ConfirmCallback acks, + com.rabbitmq.client.@Nullable ConfirmCallback nacks); /** * Delegate to the underlying dedicated channel to wait for confirms. The connection @@ -132,7 +130,7 @@ default void send(String routingKey, Message message, CorrelationData correlatio * @param correlationData data to correlate publisher confirms. * @throws AmqpException if there is a problem */ - void send(String exchange, String routingKey, Message message, CorrelationData correlationData) + void send(String exchange, String routingKey, Message message, @Nullable CorrelationData correlationData) throws AmqpException; /** @@ -206,7 +204,7 @@ void convertAndSend(String routingKey, Object message, MessagePostProcessor mess * @throws AmqpException if there is a problem */ void convertAndSend(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor, - CorrelationData correlationData) throws AmqpException; + @Nullable CorrelationData correlationData) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -269,7 +267,7 @@ Object convertSendAndReceive(String exchange, String routingKey, Object message, */ @Nullable Object convertSendAndReceive(Object message, MessagePostProcessor messagePostProcessor, - CorrelationData correlationData) throws AmqpException; + @Nullable CorrelationData correlationData) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -286,7 +284,7 @@ Object convertSendAndReceive(Object message, MessagePostProcessor messagePostPro */ @Nullable Object convertSendAndReceive(String routingKey, Object message, - MessagePostProcessor messagePostProcessor, CorrelationData correlationData) throws AmqpException; + MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -304,7 +302,7 @@ Object convertSendAndReceive(String routingKey, Object message, */ @Nullable Object convertSendAndReceive(String exchange, String routingKey, Object message, - MessagePostProcessor messagePostProcessor, CorrelationData correlationData) + @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException; /** @@ -322,8 +320,7 @@ Object convertSendAndReceive(String exchange, String routingKey, Object message, * @return the response if there is one. * @throws AmqpException if there is a problem. */ - @Nullable - T convertSendAndReceiveAsType(Object message, CorrelationData correlationData, + @Nullable T convertSendAndReceiveAsType(Object message, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -342,9 +339,8 @@ T convertSendAndReceiveAsType(Object message, CorrelationData correlationDat * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - T convertSendAndReceiveAsType(String routingKey, Object message, CorrelationData correlationData, - ParameterizedTypeReference responseType) throws AmqpException; + @Nullable T convertSendAndReceiveAsType(String routingKey, Object message, + @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -363,10 +359,9 @@ T convertSendAndReceiveAsType(String routingKey, Object message, Correlation * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - default T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, + default @Nullable T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) - throws AmqpException { + throws AmqpException { return convertSendAndReceiveAsType(exchange, routingKey, message, null, correlationData, responseType); } @@ -387,9 +382,8 @@ default T convertSendAndReceiveAsType(String exchange, String routingKey, Ob * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePostProcessor, - CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; + @Nullable T convertSendAndReceiveAsType(Object message, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -408,9 +402,8 @@ T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePo * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - T convertSendAndReceiveAsType(String routingKey, Object message, - MessagePostProcessor messagePostProcessor, CorrelationData correlationData, + @Nullable T convertSendAndReceiveAsType(String routingKey, Object message, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -431,13 +424,11 @@ T convertSendAndReceiveAsType(String routingKey, Object message, * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; - @Override default void start() { // No-op - implemented for backward compatibility diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 900d0b0b4b..dc319b3a08 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -35,11 +36,27 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.AMQP.Queue.DeclareOk; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConfirmListener; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.GetResponse; +import com.rabbitmq.client.Return; +import com.rabbitmq.client.ShutdownListener; +import com.rabbitmq.client.ShutdownSignalException; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpException; import org.springframework.amqp.AmqpIOException; -import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.AmqpMessageReturnedException; @@ -74,6 +91,10 @@ import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.rabbit.support.ValueExpression; +import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageSenderContext; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservation.DefaultRabbitTemplateObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.amqp.support.converter.SmartMessageConverter; @@ -83,13 +104,14 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MapAccessor; import org.springframework.core.ParameterizedTypeReference; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.RetryCallback; import org.springframework.retry.support.RetryTemplate; @@ -97,18 +119,6 @@ import org.springframework.util.ErrorHandler; import org.springframework.util.StringUtils; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.AMQP.Queue.DeclareOk; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConfirmListener; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.GetResponse; -import com.rabbitmq.client.Return; -import com.rabbitmq.client.ShutdownListener; -import com.rabbitmq.client.ShutdownSignalException; - /** *

* Helper class that simplifies synchronous RabbitMQ access (sending and receiving messages). @@ -147,11 +157,13 @@ * @author Mark Norkin * @author Mohammad Hewedy * @author Alexey Platonov + * @author Leonardo Ferreira + * @author Ngoc Nhan * * @since 1.0 */ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count - implements BeanFactoryAware, RabbitOperations, ChannelAwareMessageListener, + implements BeanFactoryAware, RabbitOperations, ChannelAwareMessageListener, ApplicationContextAware, ListenerContainerAware, PublisherCallbackChannel.Listener, BeanNameAware, DisposableBean { private static final String UNCHECKED = "unchecked"; @@ -174,14 +186,13 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count /* * Not static as normal since we want this TL to be scoped within the template instance. */ - private final ThreadLocal dedicatedChannels = new ThreadLocal<>(); + private final ThreadLocal<@Nullable Channel> dedicatedChannels = new ThreadLocal<>(); private final AtomicInteger activeTemplateCallbacks = new AtomicInteger(); - private final ConcurrentMap publisherConfirmChannels = - new ConcurrentHashMap(); + private final ConcurrentMap publisherConfirmChannels = new ConcurrentHashMap<>(); - private final Map replyHolder = new ConcurrentHashMap(); + private final Map replyHolder = new ConcurrentHashMap<>(); private final String uuid = UUID.randomUUID().toString(); @@ -192,17 +203,26 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private final ReplyToAddressCallback defaultReplyToAddressCallback = (request, reply) -> getReplyToAddress(request); + private final Lock fastReplyToLock = new ReentrantLock(); + private final Map directReplyToContainers = new HashMap<>(); + private final Lock directReplyToContainersLock = new ReentrantLock(); + private final AtomicInteger containerInstance = new AtomicInteger(); + private final Map consumerArgs = new HashMap<>(); + + @SuppressWarnings("NullAway.Init") + private ApplicationContext applicationContext; + private String exchange = DEFAULT_EXCHANGE; private String routingKey = DEFAULT_ROUTING_KEY; // The default queue name that will be used for synchronous receives. - private String defaultReceiveQueue; + private @Nullable String defaultReceiveQueue; private long receiveTimeout = 0; @@ -214,40 +234,39 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private String encoding = DEFAULT_ENCODING; - private String replyAddress; + private @Nullable String replyAddress; - @Nullable - private ConfirmCallback confirmCallback; + private @Nullable ConfirmCallback confirmCallback; - private ReturnsCallback returnsCallback; + private @Nullable ReturnsCallback returnsCallback; - private Expression mandatoryExpression = new ValueExpression(false); + private Expression mandatoryExpression = new ValueExpression<>(false); - private String correlationKey = null; + private @Nullable String correlationKey = null; - private RetryTemplate retryTemplate; + private @Nullable RetryTemplate retryTemplate; - private RecoveryCallback recoveryCallback; + private @Nullable RecoveryCallback recoveryCallback; - private Expression sendConnectionFactorySelectorExpression; + private @Nullable Expression sendConnectionFactorySelectorExpression; - private Expression receiveConnectionFactorySelectorExpression; + private @Nullable Expression receiveConnectionFactorySelectorExpression; private boolean useDirectReplyToContainer = true; private boolean useTemporaryReplyQueues; - private Collection beforePublishPostProcessors; + private @Nullable Collection beforePublishPostProcessors; - private Collection afterReceivePostProcessors; + private @Nullable Collection afterReceivePostProcessors; - private CorrelationDataPostProcessor correlationDataPostProcessor; + private @Nullable CorrelationDataPostProcessor correlationDataPostProcessor; - private Expression userIdExpression; + private @Nullable Expression userIdExpression; private String beanName = "rabbitTemplate"; - private Executor taskExecutor; + private @Nullable Executor taskExecutor; private boolean userCorrelationId; @@ -255,11 +274,13 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private boolean noLocalReplyConsumer; - private ErrorHandler replyErrorHandler; + private @Nullable ErrorHandler replyErrorHandler; - private volatile Boolean confirmsOrReturnsCapable; + private boolean useChannelForCorrelation; + + private boolean observationEnabled; - private volatile boolean publisherConfirms; + private @Nullable RabbitTemplateObservationConvention observationConvention; private volatile boolean usingFastReplyTo; @@ -267,18 +288,18 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private volatile boolean isListener; - private boolean useChannelForCorrelation; + private volatile boolean observationRegistryObtained; /** * Convenient constructor for use with setter injection. Don't forget to set the connection factory. */ + @SuppressWarnings("this-escape") public RabbitTemplate() { - initDefaultStrategies(); // NOSONAR - intentionally overridable; other assertions will check + initDefaultStrategies(); } /** * Create a rabbit template with default strategies and settings. - * * @param connectionFactory the connection factory to use */ public RabbitTemplate(ConnectionFactory connectionFactory) { @@ -300,10 +321,32 @@ public final void setConnectionFactory(ConnectionFactory connectionFactory) { } } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + /** - * The name of the default exchange to use for send operations when none is specified. Defaults to "" + * Enable observation via micrometer. + * @param observationEnabled true to enable. + * @since 3.0 + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + /** + * Set an observation convention; used to add additional key/values to observations. + * @param observationConvention the convention. + * @since 3.0 + */ + public void setObservationConvention(RabbitTemplateObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + + /** + * The name of the default exchange to use for send operations when none is specified. Defaults to {@code ""} * which is the default exchange in the broker (per the AMQP specification). - * * @param exchange the exchange name to use for send operations */ public void setExchange(@Nullable String exchange) { @@ -312,7 +355,6 @@ public void setExchange(@Nullable String exchange) { /** * @return the name of the default exchange used by this template. - * * @since 1.6 */ public String getExchange() { @@ -323,7 +365,6 @@ public String getExchange() { * The value of a default routing key to use for send operations when none is specified. Default is empty which is * not helpful when using the default (or any direct) exchange, but fine if the exchange is a headers exchange for * instance. - * * @param routingKey the default routing key to use for send operations */ public void setRoutingKey(String routingKey) { @@ -348,9 +389,17 @@ public void setDefaultReceiveQueue(String queue) { this.defaultReceiveQueue = queue; } + /** + * Return the configured default receive queue. + * @return the queue or null if not configured. + * @since 2.2.22 + */ + public @Nullable String getDefaultReceiveQueue() { + return this.defaultReceiveQueue; + } + /** * The encoding to use when converting between byte arrays and Strings in message properties. - * * @param encoding the encoding to set */ public void setEncoding(String encoding) { @@ -367,14 +416,14 @@ public String getEncoding() { /** * An address for replies; if not provided, a temporary exclusive, auto-delete queue will - * be used for each reply, unless RabbitMQ supports 'amq.rabbitmq.reply-to' - see - * https://www.rabbitmq.com/direct-reply-to.html + * be used for each reply, unless RabbitMQ supports + * 'amq.rabbitmq.reply-to' *

The address can be a simple queue name (in which case the reply will be routed via the default * exchange), or with the form {@code exchange/routingKey} to route the reply using an explicit * exchange and routing key. * @param replyAddress the replyAddress to set */ - public synchronized void setReplyAddress(String replyAddress) { + public void setReplyAddress(String replyAddress) { this.replyAddress = replyAddress; this.evaluatedFastReplyTo = false; } @@ -396,9 +445,7 @@ public void setReceiveTimeout(long receiveTimeout) { * sendAndReceive methods. The default value is defined as {@link #DEFAULT_REPLY_TIMEOUT}. A negative value * indicates an indefinite timeout. Not used in the plain receive methods because there is no blocking receive * operation defined in the protocol. - * * @param replyTimeout the reply timeout in milliseconds - * * @see #sendAndReceive(String, String, Message) * @see #convertSendAndReceive(String, String, Object) */ @@ -412,9 +459,7 @@ public void setReplyTimeout(long replyTimeout) { *

* The default converter is a SimpleMessageConverter, which is able to handle byte arrays, Strings, and Serializable * Objects depending on the message content type header. - * * @param messageConverter The message converter. - * * @see #convertAndSend * @see #receiveAndConvert * @see org.springframework.amqp.support.converter.SimpleMessageConverter @@ -428,7 +473,6 @@ public void setMessageConverter(MessageConverter messageConverter) { * content in the message headers and plain Java objects. In particular there are limitations when dealing with very * long string headers, which hopefully are rare in practice, but if you need to use long headers you might need to * inject a special converter here. - * * @param messagePropertiesConverter The message properties converter. */ public void setMessagePropertiesConverter(MessagePropertiesConverter messagePropertiesConverter) { @@ -437,7 +481,7 @@ public void setMessagePropertiesConverter(MessagePropertiesConverter messageProp } /** - * Return the properties converter. + * Return the converter for properties. * @return the converter. * @since 2.0 */ @@ -448,7 +492,6 @@ protected MessagePropertiesConverter getMessagePropertiesConverter() { /** * Return the message converter for this template. Useful for clients that want to take advantage of the converter * in {@link ChannelCallback} implementations. - * * @return The message converter. */ public MessageConverter getMessageConverter() { @@ -464,29 +507,7 @@ public void setConfirmCallback(ConfirmCallback confirmCallback) { /** * Set a callback to receive returned messages. * @param returnCallback the callback. - * @deprecated in favor of {@link #setReturnsCallback(ReturnsCallback)}. */ - @Deprecated - public void setReturnCallback(ReturnCallback returnCallback) { - ReturnCallback delegate = this.returnsCallback == null ? null : this.returnsCallback.delegate(); - Assert.state(this.returnsCallback == null || delegate == null || delegate.equals(returnCallback), - "Only one ReturnCallback is supported by each RabbitTemplate"); - this.returnsCallback = new ReturnsCallback() { - - @Override - public void returnedMessage(ReturnedMessage returned) { - returnCallback.returnedMessage(returned.getMessage(), returned.getReplyCode(), returned.getReplyText(), - returned.getExchange(), returned.getRoutingKey()); - } - - @Override - public ReturnCallback delegate() { - return returnCallback; - } - - }; - } - public void setReturnsCallback(ReturnsCallback returnCallback) { Assert.state(this.returnsCallback == null || this.returnsCallback.equals(returnCallback), "Only one ReturnCallback is supported by each RabbitTemplate"); @@ -610,6 +631,17 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.evaluationContext.addPropertyAccessor(new MapAccessor()); } + /** + * Return configured before post {@link MessagePostProcessor}s or {@code null}. + * @return configured before post {@link MessagePostProcessor}s or {@code null}. + * @since 3.2 + */ + public @Nullable Collection getBeforePublishPostProcessors() { + return this.beforePublishPostProcessors != null + ? Collections.unmodifiableCollection(this.beforePublishPostProcessors) + : null; + } + /** * Set {@link MessagePostProcessor}s that will be invoked immediately before invoking * {@code Channel#basicPublish()}, after all other processing, except creating the @@ -682,8 +714,7 @@ public void setAfterReceivePostProcessors(MessagePostProcessor... afterReceivePo * @return configured after receive {@link MessagePostProcessor}s or {@code null}. * @since 2.1.5 */ - @Nullable - public Collection getAfterReceivePostProcessors() { + public @Nullable Collection getAfterReceivePostProcessors() { return this.afterReceivePostProcessors != null ? Collections.unmodifiableCollection(this.afterReceivePostProcessors) : null; @@ -738,7 +769,7 @@ public void setCorrelationDataPostProcessor(CorrelationDataPostProcessor correla /** * By default, when the broker supports it and no * {@link #setReplyAddress(String) replyAddress} is provided, send/receive - * methods will use Direct reply-to (https://www.rabbitmq.com/direct-reply-to.html). + * methods will use Direct reply-to. * Setting this property to true will override that behavior and use * a temporary, auto-delete, queue for each request instead. * Changing this property has no effect once the first request has been @@ -751,7 +782,7 @@ public void setUseTemporaryReplyQueues(boolean value) { } /** - * Set whether or not to use a {@link DirectReplyToMessageListenerContainer} when + * Set whether to use a {@link DirectReplyToMessageListenerContainer} when * direct reply-to is available and being used. When false, a new consumer is created * for each request (the mechanism used in versions prior to 2.0). Default true. * @param useDirectReplyToContainer set to false to use a consumer per request. @@ -766,7 +797,7 @@ public void setUseDirectReplyToContainer(boolean useDirectReplyToContainer) { * Set an expression to be evaluated to set the userId message property if it * evaluates to a non-null value and the property is not already set in the * message to be sent. - * See https://www.rabbitmq.com/validated-user-id.html + * See validated-user-id * @param userIdExpression the expression. * @since 1.6 */ @@ -778,7 +809,7 @@ public void setUserIdExpression(Expression userIdExpression) { * Set an expression to be evaluated to set the userId message property if it * evaluates to a non-null value and the property is not already set in the * message to be sent. - * See https://www.rabbitmq.com/validated-user-id.html + * See validated-user-id * @param userIdExpression the expression. * @since 1.6 */ @@ -823,10 +854,12 @@ public boolean isUsePublisherConnection() { } /** - * To avoid deadlocked connections, it is generally recommended to use - * a separate connection for publishers and consumers (except when a publisher - * is participating in a consumer transaction). Default 'false'; will change - * to 'true' in 2.1. + * To avoid deadlocked connections, it is generally recommended to use a separate + * connection for publishers and consumers (except when a publisher is participating + * in a consumer transaction). Default 'false'. When setting this to true, be careful + * in that a {@link RabbitAdmin} that uses this template will declare queues on the + * publisher connection; this may not be what you expect, especially with exclusive + * queues that might be consumed in this application. * @param usePublisherConnection true to use a publisher connection. * @since 2.0.2 */ @@ -874,8 +907,7 @@ public void setUseChannelForCorrelation(boolean useChannelForCorrelation) { * @since 1.5 */ @Override - @Nullable - public Collection expectedQueueNames() { + public @Nullable Collection expectedQueueNames() { this.isListener = true; Collection replyQueue = null; if (this.replyAddress == null || this.replyAddress.equals(Address.AMQ_RABBITMQ_REPLY_TO)) { @@ -883,7 +915,7 @@ public Collection expectedQueueNames() { } else { Address address = new Address(this.replyAddress); - if ("".equals(address.getExchangeName())) { + if (address.getExchangeName().isEmpty()) { replyQueue = Collections.singletonList(address.getRoutingKey()); } else { @@ -902,17 +934,19 @@ public Collection expectedQueueNames() { * @return the collection of correlation data for which confirms have * not been received or null if no such confirms exist. */ - @Nullable - public Collection getUnconfirmed(long age) { + public @Nullable Collection getUnconfirmed(long age) { Set unconfirmed = new HashSet<>(); long cutoffTime = System.currentTimeMillis() - age; for (Channel channel : this.publisherConfirmChannels.keySet()) { Collection confirms = ((PublisherCallbackChannel) channel).expire(this, cutoffTime); for (PendingConfirm confirm : confirms) { - unconfirmed.add(confirm.getCorrelationData()); + CorrelationData correlationData = confirm.getCorrelationData(); + if (correlationData != null) { + unconfirmed.add(correlationData); + } } } - return unconfirmed.size() > 0 ? unconfirmed : null; + return !unconfirmed.isEmpty() ? unconfirmed : null; } /** @@ -927,6 +961,33 @@ public int getUnconfirmedCount() { .sum(); } + /** + * When using receive methods with a non-zero timeout, a + * {@link com.rabbitmq.client.Consumer} is created to receive the message. Use this + * property to add arguments to the consumer (e.g. {@code x-priority}). + * @param arg the argument name to pass into the {@code basicConsume} operation. + * @param value the argument value to pass into the {@code basicConsume} operation. + * @since 2.4.8 + * @see #removeConsumerArg(String) + */ + public void addConsumerArg(String arg, Object value) { + this.consumerArgs.put(arg, value); + } + + /** + * When using receive methods with a non-zero timeout, a + * {@link com.rabbitmq.client.Consumer} is created to receive the message. Use this + * method to remove an argument from those passed into the {@code basicConsume} + * operation. + * @param arg the argument name. + * @return the previous value. + * @since 2.4.8 + * @see #addConsumerArg(String, Object) + */ + public Object removeConsumerArg(String arg) { + return this.consumerArgs.remove(arg); + } + @Override public void start() { doStart(); @@ -942,13 +1003,17 @@ protected void doStart() { @Override public void stop() { - synchronized (this.directReplyToContainers) { + this.directReplyToContainersLock.lock(); + try { this.directReplyToContainers.values() .stream() .filter(AbstractMessageListenerContainer::isRunning) .forEach(AbstractMessageListenerContainer::stop); this.directReplyToContainers.clear(); } + finally { + this.directReplyToContainersLock.unlock(); + } doStop(); } @@ -962,11 +1027,15 @@ protected void doStop() { @Override public boolean isRunning() { - synchronized (this.directReplyToContainers) { + this.directReplyToContainersLock.lock(); + try { return this.directReplyToContainers.values() .stream() .anyMatch(AbstractMessageListenerContainer::isRunning); } + finally { + this.directReplyToContainersLock.unlock(); + } } @Override @@ -980,18 +1049,18 @@ private void evaluateFastReplyTo() { } /** - * Override this method use some other criteria to decide whether or not to use - * direct reply-to (https://www.rabbitmq.com/direct-reply-to.html). + * Override this method use some other criteria to decide whether to use + * (direct reply-to). * The default implementation returns true if the broker supports it and there * is no {@link #setReplyAddress(String) replyAddress} set and * {@link #setUseTemporaryReplyQueues(boolean) useTemporaryReplyQueues} is false. * When direct reply-to is not used, the template * will create a temporary, exclusive, auto-delete queue for the reply. *

- * This method is invoked once only - when the first message is sent, from a - * synchronized block. + * This method is invoked once only - when the first message is sent, from a locked block. * @return true to use direct reply-to. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected boolean useDirectReplyTo() { if (this.useTemporaryReplyQueues) { if (this.replyAddress != null) { @@ -1003,7 +1072,7 @@ protected boolean useDirectReplyTo() { } if (this.replyAddress == null || Address.AMQ_RABBITMQ_REPLY_TO.equals(this.replyAddress)) { try { - return execute(channel -> { // NOSONAR - never null + return execute(channel -> { channel.queueDeclarePassive(Address.AMQ_RABBITMQ_REPLY_TO); return true; }); @@ -1031,7 +1100,7 @@ private boolean shouldRethrow(AmqpException ex) { return false; } if (logger.isDebugEnabled()) { - logger.debug("IO error, deferring directReplyTo detection: " + ex.toString()); + logger.debug("IO error, deferring directReplyTo detection: " + ex); } return true; } @@ -1057,9 +1126,10 @@ public void send(final String exchange, final String routingKey, final Message m } @Override - public void send(final String exchange, final String routingKey, - final Message message, @Nullable final CorrelationData correlationData) + public void send(@Nullable String exchange, @Nullable String routingKey, + final Message message, @Nullable CorrelationData correlationData) throws AmqpException { + execute(channel -> { doSend(channel, exchange, routingKey, message, (RabbitTemplate.this.returnsCallback != null @@ -1070,23 +1140,23 @@ && isMandatoryFor(message), }, obtainTargetConnectionFactory(this.sendConnectionFactorySelectorExpression, message)); } - private ConnectionFactory obtainTargetConnectionFactory(Expression expression, Object rootObject) { - if (expression != null && getConnectionFactory() instanceof AbstractRoutingConnectionFactory) { - AbstractRoutingConnectionFactory routingConnectionFactory = - (AbstractRoutingConnectionFactory) getConnectionFactory(); + private ConnectionFactory obtainTargetConnectionFactory(@Nullable Expression expression, + @Nullable Object rootObject) { + + if (expression != null && getConnectionFactory() instanceof AbstractRoutingConnectionFactory routingCF) { Object lookupKey; if (rootObject != null) { - lookupKey = this.sendConnectionFactorySelectorExpression.getValue(this.evaluationContext, rootObject); + lookupKey = expression.getValue(this.evaluationContext, rootObject); } else { - lookupKey = this.sendConnectionFactorySelectorExpression.getValue(this.evaluationContext); + lookupKey = expression.getValue(this.evaluationContext); } if (lookupKey != null) { - ConnectionFactory connectionFactory = routingConnectionFactory.getTargetConnectionFactory(lookupKey); + ConnectionFactory connectionFactory = routingCF.getTargetConnectionFactory(lookupKey); if (connectionFactory != null) { return connectionFactory; } - else if (!routingConnectionFactory.isLenientFallback()) { + else if (!routingCF.isLenientFallback()) { throw new IllegalStateException("Cannot determine target ConnectionFactory for lookup key [" + lookupKey + "]"); } @@ -1113,6 +1183,7 @@ public void convertAndSend(String routingKey, final Object object) throws AmqpEx @Override public void convertAndSend(String routingKey, final Object object, CorrelationData correlationData) throws AmqpException { + convertAndSend(this.exchange, routingKey, object, correlationData); } @@ -1136,6 +1207,7 @@ public void convertAndSend(Object message, MessagePostProcessor messagePostProce @Override public void convertAndSend(String routingKey, Object message, MessagePostProcessor messagePostProcessor) throws AmqpException { + convertAndSend(this.exchange, routingKey, message, messagePostProcessor, null); } @@ -1143,6 +1215,7 @@ public void convertAndSend(String routingKey, Object message, MessagePostProcess public void convertAndSend(Object message, MessagePostProcessor messagePostProcessor, CorrelationData correlationData) throws AmqpException { + convertAndSend(this.exchange, this.routingKey, message, messagePostProcessor, correlationData); } @@ -1150,12 +1223,14 @@ public void convertAndSend(Object message, MessagePostProcessor messagePostProce public void convertAndSend(String routingKey, Object message, MessagePostProcessor messagePostProcessor, CorrelationData correlationData) throws AmqpException { + convertAndSend(this.exchange, routingKey, message, messagePostProcessor, correlationData); } @Override public void convertAndSend(String exchange, String routingKey, final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { + convertAndSend(exchange, routingKey, message, messagePostProcessor, null); } @@ -1163,6 +1238,7 @@ public void convertAndSend(String exchange, String routingKey, final Object mess public void convertAndSend(String exchange, String routingKey, final Object message, final MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException { + Message messageToSend = convertMessageIfNecessary(message); messageToSend = messagePostProcessor.postProcessMessage(messageToSend, correlationData, nullSafeExchange(exchange), nullSafeRoutingKey(routingKey)); @@ -1170,14 +1246,12 @@ public void convertAndSend(String exchange, String routingKey, final Object mess } @Override - @Nullable - public Message receive() throws AmqpException { - return this.receive(getRequiredQueue()); + public @Nullable Message receive() throws AmqpException { + return receive(getRequiredQueue()); } @Override - @Nullable - public Message receive(String queueName) { + public @Nullable Message receive(String queueName) { if (this.receiveTimeout == 0) { return doReceiveNoWait(queueName); } @@ -1192,8 +1266,7 @@ public Message receive(String queueName) { * @return The message, or null if none immediately available. * @since 1.5 */ - @Nullable - protected Message doReceiveNoWait(final String queueName) { + protected @Nullable Message doReceiveNoWait(final String queueName) { Message message = execute(channel -> { GetResponse response = channel.basicGet(queueName, !isChannelTransacted()); // Response can be null is the case that there is no message on the queue. @@ -1204,7 +1277,7 @@ protected Message doReceiveNoWait(final String queueName) { channel.txCommit(); } else if (isChannelTransacted()) { - // Not locally transacted but it is transacted so it + // Not locally transacted, but it is transacted, so it // could be synchronized with an external transaction ConnectionFactoryUtils.registerDeliveryTag(getConnectionFactory(), channel, deliveryTag); } @@ -1213,13 +1286,12 @@ else if (isChannelTransacted()) { } return null; }, obtainTargetConnectionFactory(this.receiveConnectionFactorySelectorExpression, queueName)); - logReceived(message); + logReceived("Received: ", message); return message; } @Override - @Nullable - public Message receive(long timeoutMillis) throws AmqpException { + public @Nullable Message receive(long timeoutMillis) throws AmqpException { String queue = getRequiredQueue(); if (timeoutMillis == 0) { return doReceiveNoWait(queue); @@ -1230,8 +1302,7 @@ public Message receive(long timeoutMillis) throws AmqpException { } @Override - @Nullable - public Message receive(final String queueName, final long timeoutMillis) { + public @Nullable Message receive(final String queueName, final long timeoutMillis) { Message message = execute(channel -> { Delivery delivery = consumeDelivery(channel, queueName, timeoutMillis); if (delivery == null) { @@ -1251,61 +1322,59 @@ else if (isChannelTransacted()) { } return buildMessageFromDelivery(delivery); } - }); - logReceived(message); + }, obtainTargetConnectionFactory(this.receiveConnectionFactorySelectorExpression, null)); + logReceived("Received: ", message); return message; } @Override - @Nullable - public Object receiveAndConvert() throws AmqpException { - return receiveAndConvert(this.getRequiredQueue()); + public @Nullable Object receiveAndConvert() throws AmqpException { + return receiveAndConvert(getRequiredQueue()); } @Override - @Nullable - public Object receiveAndConvert(String queueName) throws AmqpException { + public @Nullable Object receiveAndConvert(String queueName) throws AmqpException { return receiveAndConvert(queueName, this.receiveTimeout); } @Override - @Nullable - public Object receiveAndConvert(long timeoutMillis) throws AmqpException { - return receiveAndConvert(this.getRequiredQueue(), timeoutMillis); + public @Nullable Object receiveAndConvert(long timeoutMillis) throws AmqpException { + return receiveAndConvert(getRequiredQueue(), timeoutMillis); } @Override - @Nullable - public Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException { + public @Nullable Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException { Message response = timeoutMillis == 0 ? doReceiveNoWait(queueName) : receive(queueName, timeoutMillis); if (response != null) { - return getRequiredMessageConverter().fromMessage(response); + return getMessageConverter().fromMessage(response); } return null; } @Override - @Nullable - public T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException { - return receiveAndConvert(this.getRequiredQueue(), type); + public @Nullable T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException { + return receiveAndConvert(getRequiredQueue(), type); } @Override - @Nullable - public T receiveAndConvert(String queueName, ParameterizedTypeReference type) throws AmqpException { + public @Nullable T receiveAndConvert(String queueName, ParameterizedTypeReference type) + throws AmqpException { + return receiveAndConvert(queueName, this.receiveTimeout, type); } @Override - @Nullable - public T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { - return receiveAndConvert(this.getRequiredQueue(), timeoutMillis, type); + public @Nullable T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) + throws AmqpException { + + return receiveAndConvert(getRequiredQueue(), timeoutMillis, type); } @Override @SuppressWarnings(UNCHECKED) - @Nullable - public T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { + public @Nullable T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) + throws AmqpException { + Message response = timeoutMillis == 0 ? doReceiveNoWait(queueName) : receive(queueName, timeoutMillis); if (response != null) { return (T) getRequiredSmartMessageConverter().fromMessage(response, type); @@ -1320,7 +1389,9 @@ public boolean receiveAndReply(ReceiveAndReplyCallback callback) th @Override @SuppressWarnings(UNCHECKED) - public boolean receiveAndReply(final String queueName, ReceiveAndReplyCallback callback) throws AmqpException { + public boolean receiveAndReply(final String queueName, ReceiveAndReplyCallback callback) + throws AmqpException { + return receiveAndReply(queueName, callback, (ReplyToAddressCallback) this.defaultReplyToAddressCallback); } @@ -1344,12 +1415,13 @@ public boolean receiveAndReply(final String queueName, ReceiveAndReplyCal public boolean receiveAndReply(ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) throws AmqpException { - return receiveAndReply(this.getRequiredQueue(), callback, replyToAddressCallback); + return receiveAndReply(getRequiredQueue(), callback, replyToAddressCallback); } @Override public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) throws AmqpException { + return doReceiveAndReply(queueName, callback, replyToAddressCallback); } @@ -1363,7 +1435,7 @@ private boolean doReceiveAndReply(final String queueName, final ReceiveAn } return false; }, obtainTargetConnectionFactory(this.receiveConnectionFactorySelectorExpression, queueName)); - return result == null ? false : result; + return result != null && result; } @Nullable @@ -1381,7 +1453,7 @@ private Message receiveForReply(final String queueName, Channel channel) throws channel.basicAck(deliveryTag1, false); } else if (channelTransacted) { - // Not locally transacted but it is transacted so it could be + // Not locally transacted, but it is transacted, so it could be // synchronized with an external transaction ConnectionFactoryUtils.registerDeliveryTag(getConnectionFactory(), channel, deliveryTag1); } @@ -1393,7 +1465,7 @@ else if (channelTransacted) { if (delivery != null) { long deliveryTag2 = delivery.getEnvelope().getDeliveryTag(); if (channelTransacted && !channelLocallyTransacted) { - // Not locally transacted but it is transacted so it could be + // Not locally transacted, but it is transacted, so it could be // synchronized with an external transaction ConnectionFactoryUtils.registerDeliveryTag(getConnectionFactory(), channel, deliveryTag2); } @@ -1403,12 +1475,11 @@ else if (channelTransacted) { receiveMessage = buildMessageFromDelivery(delivery); } } - logReceived(receiveMessage); + logReceived("Received: ", receiveMessage); return receiveMessage; } - @Nullable // NOSONAR complexity - private Delivery consumeDelivery(Channel channel, String queueName, long timeoutMillis) + private @Nullable Delivery consumeDelivery(Channel channel, String queueName, long timeoutMillis) throws IOException { Delivery delivery = null; @@ -1458,12 +1529,16 @@ private Delivery consumeDelivery(Channel channel, String queueName, long timeout return delivery; } - private void logReceived(@Nullable Message message) { - if (message == null) { - logger.debug("Received no message"); - } - else if (logger.isDebugEnabled()) { - logger.debug("Received: " + message); + /** + * Log a received message. The default implementation logs the full message at DEBUG + * level. Override this method to change that behavior. + * @param prefix a prefix, e.g. "Received: " or "Reply: ". + * @param message the message. + * @since 2.4.6 + */ + protected void logReceived(String prefix, @Nullable Message message) { + if (logger.isDebugEnabled()) { + logger.debug(prefix + Objects.requireNonNullElse(message, "no message")); } } @@ -1474,7 +1549,7 @@ private boolean sendReply(final ReceiveAndReplyCallback callback, Object receive = receiveMessage; if (!(ReceiveAndReplyMessageCallback.class.isAssignableFrom(callback.getClass()))) { - receive = getRequiredMessageConverter().fromMessage(receiveMessage); + receive = getMessageConverter().fromMessage(receiveMessage); } S reply; @@ -1483,7 +1558,9 @@ private boolean sendReply(final ReceiveAndReplyCallback callback, } catch (ClassCastException e) { StackTraceElement[] trace = e.getStackTrace(); - if (trace[0].getMethodName().equals("handle") && trace[1].getFileName().equals("RabbitTemplate.java")) { + if (trace[0].getMethodName().equals("handle") + && Objects.equals(trace[1].getFileName(), "RabbitTemplate.java")) { + throw new IllegalArgumentException("ReceiveAndReplyCallback '" + callback + "' can't handle received object '" + receive + "'", e); } @@ -1503,7 +1580,7 @@ else if (isChannelLocallyTransacted(channel)) { } private void doSendReply(final ReplyToAddressCallback replyToAddressCallback, Channel channel, - Message receiveMessage, S reply) throws IOException { + Message receiveMessage, S reply) { Address replyTo = replyToAddressCallback.getReplyToAddress(receiveMessage, reply); @@ -1524,7 +1601,9 @@ private void doSendReply(final ReplyToAddressCallback replyToAddressCallb correlation = messageId; } } - replyMessageProperties.setCorrelationId((String) correlation); + if (correlation != null) { + replyMessageProperties.setCorrelationId((String) correlation); + } } else { replyMessageProperties.setHeader(this.correlationKey, correlation); @@ -1540,116 +1619,101 @@ private void doSendReply(final ReplyToAddressCallback replyToAddressCallb } @Override - @Nullable - public Message sendAndReceive(final Message message) throws AmqpException { + public @Nullable Message sendAndReceive(final Message message) throws AmqpException { return sendAndReceive(message, null); } - @Nullable - public Message sendAndReceive(final Message message, @Nullable CorrelationData correlationData) + public @Nullable Message sendAndReceive(final Message message, @Nullable CorrelationData correlationData) throws AmqpException { return doSendAndReceive(this.exchange, this.routingKey, message, correlationData); } @Override - @Nullable - public Message sendAndReceive(final String routingKey, final Message message) throws AmqpException { + public @Nullable Message sendAndReceive(final String routingKey, final Message message) throws AmqpException { return sendAndReceive(routingKey, message, null); } - @Nullable - public Message sendAndReceive(final String routingKey, final Message message, + public @Nullable Message sendAndReceive(final String routingKey, final Message message, @Nullable CorrelationData correlationData) throws AmqpException { return doSendAndReceive(this.exchange, routingKey, message, correlationData); } @Override - @Nullable - public Message sendAndReceive(final String exchange, final String routingKey, final Message message) + public @Nullable Message sendAndReceive(final String exchange, final String routingKey, final Message message) throws AmqpException { return sendAndReceive(exchange, routingKey, message, null); } - @Nullable - public Message sendAndReceive(final String exchange, final String routingKey, final Message message, + public @Nullable Message sendAndReceive(final String exchange, final String routingKey, final Message message, @Nullable CorrelationData correlationData) throws AmqpException { return doSendAndReceive(exchange, routingKey, message, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final Object message) throws AmqpException { + public @Nullable Object convertSendAndReceive(final Object message) throws AmqpException { return convertSendAndReceive(message, (CorrelationData) null); } @Override - @Nullable - public Object convertSendAndReceive(final Object message, @Nullable CorrelationData correlationData) + public @Nullable Object convertSendAndReceive(final Object message, @Nullable CorrelationData correlationData) throws AmqpException { return convertSendAndReceive(this.exchange, this.routingKey, message, null, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final String routingKey, final Object message) throws AmqpException { + public @Nullable Object convertSendAndReceive(final String routingKey, final Object message) throws AmqpException { return convertSendAndReceive(routingKey, message, (CorrelationData) null); } @Override - @Nullable - public Object convertSendAndReceive(final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String routingKey, final Object message, @Nullable CorrelationData correlationData) throws AmqpException { return convertSendAndReceive(this.exchange, routingKey, message, null, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final String exchange, final String routingKey, final Object message) + public @Nullable Object convertSendAndReceive(final String exchange, final String routingKey, final Object message) throws AmqpException { return convertSendAndReceive(exchange, routingKey, message, (CorrelationData) null); } @Override - @Nullable - public Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, @Nullable CorrelationData correlationData) throws AmqpException { return convertSendAndReceive(exchange, routingKey, message, null, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final Object message, final MessagePostProcessor messagePostProcessor) + public @Nullable Object convertSendAndReceive(final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { + return convertSendAndReceive(message, messagePostProcessor, null); } @Override - @Nullable - public Object convertSendAndReceive(final Object message, final MessagePostProcessor messagePostProcessor, + public @Nullable Object convertSendAndReceive(final Object message, final MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException { return convertSendAndReceive(this.exchange, this.routingKey, message, messagePostProcessor, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { return convertSendAndReceive(routingKey, message, messagePostProcessor, null); } @Override - @Nullable - public Object convertSendAndReceive(final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException { @@ -1657,36 +1721,34 @@ public Object convertSendAndReceive(final String routingKey, final Object messag } @Override - @Nullable - public Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { + return convertSendAndReceive(exchange, routingKey, message, messagePostProcessor, null); } @Override - @Nullable - public Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, - @Nullable final CorrelationData correlationData) throws AmqpException { + public @Nullable Object convertSendAndReceive(String exchange, String routingKey, Object message, + @Nullable MessagePostProcessor messagePostProcessor, + @Nullable CorrelationData correlationData) throws AmqpException { Message replyMessage = convertSendAndReceiveRaw(exchange, routingKey, message, messagePostProcessor, correlationData); if (replyMessage == null) { return null; } - return this.getRequiredMessageConverter().fromMessage(replyMessage); + return getMessageConverter().fromMessage(replyMessage); } @Override - @Nullable - public T convertSendAndReceiveAsType(final Object message, ParameterizedTypeReference responseType) + public @Nullable T convertSendAndReceiveAsType(final Object message, ParameterizedTypeReference responseType) throws AmqpException { + return convertSendAndReceiveAsType(message, (CorrelationData) null, responseType); } @Override - @Nullable - public T convertSendAndReceiveAsType(final Object message, @Nullable CorrelationData correlationData, + public @Nullable T convertSendAndReceiveAsType(final Object message, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { return convertSendAndReceiveAsType(this.exchange, this.routingKey, message, null, correlationData, @@ -1694,16 +1756,14 @@ public T convertSendAndReceiveAsType(final Object message, @Nullable Correla } @Override - @Nullable - public T convertSendAndReceiveAsType(final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String routingKey, final Object message, ParameterizedTypeReference responseType) throws AmqpException { return convertSendAndReceiveAsType(routingKey, message, (CorrelationData) null, responseType); } @Override - @Nullable - public T convertSendAndReceiveAsType(final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String routingKey, final Object message, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { @@ -1711,16 +1771,14 @@ public T convertSendAndReceiveAsType(final String routingKey, final Object m } @Override - @Nullable - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String exchange, final String routingKey, Object message, ParameterizedTypeReference responseType) throws AmqpException { return convertSendAndReceiveAsType(exchange, routingKey, message, (CorrelationData) null, responseType); } @Override - @Nullable - public T convertSendAndReceiveAsType(final Object message, + public @Nullable T convertSendAndReceiveAsType(final Object message, @Nullable final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException { @@ -1728,9 +1786,8 @@ public T convertSendAndReceiveAsType(final Object message, } @Override - @Nullable - public T convertSendAndReceiveAsType(final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, + public @Nullable T convertSendAndReceiveAsType(final Object message, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { @@ -1739,8 +1796,7 @@ public T convertSendAndReceiveAsType(final Object message, } @Override - @Nullable - public T convertSendAndReceiveAsType(final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String routingKey, final Object message, @Nullable final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException { @@ -1748,9 +1804,8 @@ public T convertSendAndReceiveAsType(final String routingKey, final Object m } @Override - @Nullable - public T convertSendAndReceiveAsType(final String routingKey, final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, + public @Nullable T convertSendAndReceiveAsType(final String routingKey, final Object message, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { return convertSendAndReceiveAsType(this.exchange, routingKey, message, messagePostProcessor, correlationData, @@ -1758,8 +1813,7 @@ public T convertSendAndReceiveAsType(final String routingKey, final Object m } @Override - @Nullable - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String exchange, final String routingKey, Object message, final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException { @@ -1768,10 +1822,9 @@ public T convertSendAndReceiveAsType(final String exchange, final String rou @Override @SuppressWarnings(UNCHECKED) - @Nullable - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, @Nullable final CorrelationData correlationData, - ParameterizedTypeReference responseType) throws AmqpException { + public @Nullable T convertSendAndReceiveAsType(@Nullable String exchange, @Nullable String routingKey, + Object message, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { Message replyMessage = convertSendAndReceiveRaw(exchange, routingKey, message, messagePostProcessor, correlationData); @@ -1793,10 +1846,9 @@ public T convertSendAndReceiveAsType(final String exchange, final String rou * @return the reply message or null if a timeout occurs. * @since 1.6.6 */ - @Nullable - protected Message convertSendAndReceiveRaw(final String exchange, final String routingKey, final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, - @Nullable final CorrelationData correlationData) { + protected @Nullable Message convertSendAndReceiveRaw(@Nullable String exchange, @Nullable String routingKey, + Object message, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable CorrelationData correlationData) { Message requestMessage = convertMessageIfNecessary(message); if (messagePostProcessor != null) { @@ -1807,10 +1859,10 @@ protected Message convertSendAndReceiveRaw(final String exchange, final String r } protected Message convertMessageIfNecessary(final Object object) { - if (object instanceof Message) { - return (Message) object; + if (object instanceof Message msg) { + return msg; } - return getRequiredMessageConverter().toMessage(object, new MessageProperties()); + return getMessageConverter().toMessage(object, new MessageProperties()); } /** @@ -1822,16 +1874,19 @@ protected Message convertMessageIfNecessary(final Object object) { * @param correlationData the correlation data for confirms * @return the message that is received in reply */ - @Nullable - protected Message doSendAndReceive(final String exchange, final String routingKey, final Message message, - @Nullable CorrelationData correlationData) { + protected @Nullable Message doSendAndReceive(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData) { if (!this.evaluatedFastReplyTo) { - synchronized (this) { + this.fastReplyToLock.lock(); + try { if (!this.evaluatedFastReplyTo) { evaluateFastReplyTo(); } } + finally { + this.fastReplyToLock.unlock(); + } } if (this.usingFastReplyTo && this.useDirectReplyToContainer) { @@ -1845,9 +1900,8 @@ else if (this.replyAddress == null || this.usingFastReplyTo) { } } - @Nullable - protected Message doSendAndReceiveWithTemporary(final String exchange, final String routingKey, - final Message message, @Nullable final CorrelationData correlationData) { + protected @Nullable Message doSendAndReceiveWithTemporary(@Nullable String exchange, @Nullable String routingKey, + final Message message, @Nullable CorrelationData correlationData) { return execute(channel -> { final PendingReply pendingReply = new PendingReply(); @@ -1896,7 +1950,7 @@ public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProp }; channel.addShutdownListener(shutdownListener); channel.basicConsume(replyTo, true, consumerTag, this.noLocalReplyConsumer, true, null, consumer); - Message reply = null; + Message reply; try { reply = exchangeMessages(exchange, routingKey, message, correlationData, channel, pendingReply, messageTag); @@ -1921,20 +1975,19 @@ private void cancelConsumerQuietly(Channel channel, DefaultConsumer consumer) { RabbitUtils.cancel(channel, consumer.getConsumerTag()); } - @Nullable - protected Message doSendAndReceiveWithFixed(final String exchange, final String routingKey, final Message message, - @Nullable final CorrelationData correlationData) { + protected @Nullable Message doSendAndReceiveWithFixed(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData) { Assert.state(this.isListener, () -> "RabbitTemplate is not configured as MessageListener - " + "cannot use a 'replyAddress': " + this.replyAddress); - return execute(channel -> { - return doSendAndReceiveAsListener(exchange, routingKey, message, correlationData, channel, false); - }, obtainTargetConnectionFactory(this.sendConnectionFactorySelectorExpression, message)); + return execute(channel -> + doSendAndReceiveAsListener(exchange, routingKey, message, correlationData, channel, false), + obtainTargetConnectionFactory(this.sendConnectionFactorySelectorExpression, message)); } - @Nullable - private Message doSendAndReceiveWithDirect(String exchange, String routingKey, Message message, - @Nullable CorrelationData correlationData) { + private @Nullable Message doSendAndReceiveWithDirect(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData) { + ConnectionFactory connectionFactory = obtainTargetConnectionFactory( this.sendConnectionFactorySelectorExpression, message); if (this.usePublisherConnection && connectionFactory.getPublisherConnectionFactory() != null) { @@ -1948,7 +2001,7 @@ private Message doSendAndReceiveWithDirect(String exchange, String routingKey, M boolean cancelConsumer = false; try { Channel channel = channelHolder.getChannel(); - if (this.confirmsOrReturnsCapable) { + if (isPublisherConfirmsOrReturns(connectionFactory)) { // NOSONAR false positive NP dereference addListener(channel); } Message reply = doSendAndReceiveAsListener(exchange, routingKey, message, correlationData, channel, @@ -1971,7 +2024,8 @@ private Message doSendAndReceiveWithDirect(String exchange, String routingKey, M private DirectReplyToMessageListenerContainer createReplyToContainer(ConnectionFactory connectionFactory) { DirectReplyToMessageListenerContainer container; - synchronized (this.directReplyToContainers) { + this.directReplyToContainersLock.lock(); + try { container = this.directReplyToContainers.get(connectionFactory); if (container == null) { container = new DirectReplyToMessageListenerContainer(connectionFactory); @@ -1989,12 +2043,15 @@ private DirectReplyToMessageListenerContainer createReplyToContainer(ConnectionF this.replyAddress = Address.AMQ_RABBITMQ_REPLY_TO; } } + finally { + this.directReplyToContainersLock.unlock(); + } return container; } - @Nullable - private Message doSendAndReceiveAsListener(final String exchange, final String routingKey, final Message message, - @Nullable final CorrelationData correlationData, Channel channel, boolean noCorrelation) throws Exception { // NOSONAR + private @Nullable Message doSendAndReceiveAsListener(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData, Channel channel, boolean noCorrelation) + throws Exception { // NOSONAR final PendingReply pendingReply = new PendingReply(); String messageTag = null; @@ -2018,7 +2075,7 @@ private Message doSendAndReceiveAsListener(final String exchange, final String r if (logger.isDebugEnabled()) { logger.debug("Sending message with tag " + messageTag); } - Message reply = null; + Message reply; try { reply = exchangeMessages(exchange, routingKey, message, correlationData, channel, pendingReply, messageTag); @@ -2069,10 +2126,9 @@ private void saveAndSetProperties(final Message message, final PendingReply pend } } - @Nullable - private Message exchangeMessages(final String exchange, final String routingKey, final Message message, - @Nullable final CorrelationData correlationData, Channel channel, final PendingReply pendingReply, - String messageTag) throws IOException, InterruptedException { + private @Nullable Message exchangeMessages(@Nullable String exchange, @Nullable String routingKey, Message message, + @Nullable CorrelationData correlationData, Channel channel, final PendingReply pendingReply, + String messageTag) throws InterruptedException { Message reply; boolean mandatory = isMandatoryFor(message); @@ -2081,9 +2137,7 @@ private Message exchangeMessages(final String exchange, final String routingKey, } doSend(channel, exchange, routingKey, message, mandatory, correlationData); reply = this.replyTimeout < 0 ? pendingReply.get() : pendingReply.get(this.replyTimeout, TimeUnit.MILLISECONDS); - if (this.logger.isDebugEnabled()) { - this.logger.debug("Reply: " + reply); - } + logReceived("Reply: ", reply); if (reply == null) { replyTimedOut(message.getMessageProperties().getCorrelationId()); } @@ -2095,7 +2149,7 @@ private Message exchangeMessages(final String exchange, final String routingKey, * @param correlationId the correlationId * @since 2.1.2 */ - protected void replyTimedOut(String correlationId) { + protected void replyTimedOut(@Nullable String correlationId) { // NOSONAR } @@ -2111,14 +2165,12 @@ public Boolean isMandatoryFor(final Message message) { } @Override - @Nullable - public T execute(ChannelCallback action) { + public @Nullable T execute(ChannelCallback action) { return execute(action, getConnectionFactory()); } @SuppressWarnings(UNCHECKED) - @Nullable - private T execute(final ChannelCallback action, final ConnectionFactory connectionFactory) { + private @Nullable T execute(final ChannelCallback action, final ConnectionFactory connectionFactory) { if (this.retryTemplate != null) { try { return this.retryTemplate.execute( @@ -2137,8 +2189,7 @@ private T execute(final ChannelCallback action, final ConnectionFactory c } } - @Nullable - private T doExecute(ChannelCallback action, ConnectionFactory connectionFactory) { // NOSONAR complexity + private @Nullable T doExecute(ChannelCallback action, ConnectionFactory connectionFactory) { // NOSONAR complexity Assert.notNull(action, "Callback object must not be null"); Channel channel = null; boolean invokeScope = false; @@ -2162,14 +2213,8 @@ private T doExecute(ChannelCallback action, ConnectionFactory connectionF else { connection = ConnectionFactoryUtils.createConnection(connectionFactory, this.usePublisherConnection); // NOSONAR - RabbitUtils closes - if (connection == null) { - throw new IllegalStateException("Connection factory returned a null connection"); - } try { channel = connection.createChannel(false); - if (channel == null) { - throw new IllegalStateException("Connection returned a null channel"); - } } catch (RuntimeException e) { RabbitUtils.closeConnection(connection); @@ -2184,7 +2229,7 @@ private T doExecute(ChannelCallback action, ConnectionFactory connectionF return invokeAction(action, connectionFactory, channel); } catch (Exception ex) { - if (isChannelLocallyTransacted(channel)) { + if (isChannelLocallyTransacted(channel) && resourceHolder != null) { resourceHolder.rollbackAll(); } throw convertRabbitAccessException(ex); @@ -2208,16 +2253,13 @@ private void cleanUpAfterAction(@Nullable Channel channel, boolean invokeScope, } } - @Nullable - private T invokeAction(ChannelCallback action, ConnectionFactory connectionFactory, Channel channel) + private @Nullable T invokeAction(ChannelCallback action, ConnectionFactory connectionFactory, Channel channel) throws Exception { // NOSONAR see the callback - if (this.confirmsOrReturnsCapable == null) { - determineConfirmsReturnsCapability(connectionFactory); - } - if (this.confirmsOrReturnsCapable) { + if (isPublisherConfirmsOrReturns(connectionFactory)) { addListener(channel); } + if (logger.isDebugEnabled()) { logger.debug( "Executing callback " + action.getClass().getSimpleName() + " on RabbitMQ Channel: " + channel); @@ -2226,9 +2268,8 @@ private T invokeAction(ChannelCallback action, ConnectionFactory connecti } @Override - @Nullable - public T invoke(OperationsCallback action, @Nullable com.rabbitmq.client.ConfirmCallback acks, - @Nullable com.rabbitmq.client.ConfirmCallback nacks) { + public @Nullable T invoke(OperationsCallback action, com.rabbitmq.client.@Nullable ConfirmCallback acks, + com.rabbitmq.client.@Nullable ConfirmCallback nacks) { final Channel currentChannel = this.dedicatedChannels.get(); Assert.state(currentChannel == null, () -> "Nested invoke() calls are not supported; channel '" + currentChannel @@ -2251,16 +2292,10 @@ public T invoke(OperationsCallback action, @Nullable com.rabbitmq.client. if (this.usePublisherConnection && connectionFactory.getPublisherConnectionFactory() != null) { connectionFactory = connectionFactory.getPublisherConnectionFactory(); } - connection = connectionFactory.createConnection(); // NOSONAR - RabbitUtils - if (connection == null) { - throw new IllegalStateException("Connection factory returned a null connection"); - } + connection = connectionFactory.createConnection(); try { channel = connection.createChannel(false); - if (channel == null) { - throw new IllegalStateException("Connection returned a null channel"); - } - if (!connectionFactory.isPublisherConfirms()) { + if (!connectionFactory.isPublisherConfirms() && !connectionFactory.isSimplePublisherConfirms()) { RabbitUtils.setPhysicalCloseRequired(channel, true); } this.dedicatedChannels.set(channel); @@ -2279,12 +2314,12 @@ public T invoke(OperationsCallback action, @Nullable com.rabbitmq.client. } } - @Nullable - private ConfirmListener addConfirmListener(@Nullable com.rabbitmq.client.ConfirmCallback acks, - @Nullable com.rabbitmq.client.ConfirmCallback nacks, Channel channel) { + private @Nullable ConfirmListener addConfirmListener(com.rabbitmq.client.@Nullable ConfirmCallback acks, + com.rabbitmq.client.@Nullable ConfirmCallback nacks, Channel channel) { + ConfirmListener listener = null; - if (acks != null && nacks != null && channel instanceof ChannelProxy - && ((ChannelProxy) channel).isConfirmSelected()) { + if (acks != null && nacks != null && channel instanceof ChannelProxy proxy + && proxy.isConfirmSelected()) { listener = channel.addConfirmListener(acks, nacks); } return listener; @@ -2293,7 +2328,7 @@ private ConfirmListener addConfirmListener(@Nullable com.rabbitmq.client.Confirm private void cleanUpAfterAction(@Nullable RabbitResourceHolder resourceHolder, @Nullable Connection connection, @Nullable Channel channel, @Nullable ConfirmListener listener) { - if (listener != null) { + if (channel != null && listener != null) { channel.removeConfirmListener(listener); } this.activeTemplateCallbacks.decrementAndGet(); @@ -2339,10 +2374,8 @@ public void waitForConfirmsOrDie(long timeout) { } } - public void determineConfirmsReturnsCapability(ConnectionFactory connectionFactory) { - this.publisherConfirms = connectionFactory.isPublisherConfirms(); - this.confirmsOrReturnsCapable = - this.publisherConfirms || connectionFactory.isPublisherReturns(); + private boolean isPublisherConfirmsOrReturns(ConnectionFactory connectionFactory) { + return connectionFactory.isPublisherConfirms() || connectionFactory.isPublisherReturns(); } /** @@ -2354,10 +2387,9 @@ public void determineConfirmsReturnsCapability(ConnectionFactory connectionFacto * @param message The Message to send. * @param mandatory The mandatory flag. * @param correlationData The correlation data. - * @throws IOException If thrown by RabbitMQ API methods. */ - public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Message message, - boolean mandatory, @Nullable CorrelationData correlationData) throws IOException { + public void doSend(Channel channel, @Nullable String exchangeArg, @Nullable String routingKeyArg, Message message, + boolean mandatory, @Nullable CorrelationData correlationData) { String exch = nullSafeExchange(exchangeArg); String rKey = nullSafeRoutingKey(routingKeyArg); @@ -2387,7 +2419,7 @@ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Me logger.debug("Publishing message [" + messageToUse + "] on exchange [" + exch + "], routingKey = [" + rKey + "]"); } - sendToRabbit(channel, exch, rKey, mandatory, messageToUse); + observeTheSend(channel, messageToUse, mandatory, exch, rKey); // Check if commit needed if (isChannelLocallyTransacted(channel)) { // Transacted channel created by this template -> commit. @@ -2395,13 +2427,27 @@ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Me } } + protected void observeTheSend(Channel channel, Message message, boolean mandatory, String exch, String rKey) { + + if (!this.observationRegistryObtained && this.observationEnabled) { + obtainObservationRegistry(this.applicationContext); + this.observationRegistryObtained = true; + } + ObservationRegistry registry = getObservationRegistry(); + Observation observation = RabbitTemplateObservation.TEMPLATE_OBSERVATION.observation(this.observationConvention, + DefaultRabbitTemplateObservationConvention.INSTANCE, + () -> new RabbitMessageSenderContext(message, this.beanName, exch, rKey), registry); + + observation.observe(() -> sendToRabbit(channel, exch, rKey, mandatory, message)); + } + /** * Return the exchange or the default exchange if null. * @param exchange the exchange. * @return the result. * @since 2.3.4 */ - public String nullSafeExchange(String exchange) { + public String nullSafeExchange(@Nullable String exchange) { return exchange == null ? this.exchange : exchange; } @@ -2411,34 +2457,49 @@ public String nullSafeExchange(String exchange) { * @return the result. * @since 2.3.4 */ - public String nullSafeRoutingKey(String rk) { + public String nullSafeRoutingKey(@Nullable String rk) { return rk == null ? this.routingKey : rk; } protected void sendToRabbit(Channel channel, String exchange, String routingKey, boolean mandatory, - Message message) throws IOException { + Message message) { + BasicProperties convertedMessageProperties = this.messagePropertiesConverter .fromMessageProperties(message.getMessageProperties(), this.encoding); - channel.basicPublish(exchange, routingKey, mandatory, convertedMessageProperties, message.getBody()); + try { + channel.basicPublish(exchange, routingKey, mandatory, convertedMessageProperties, message.getBody()); + } + catch (IOException ex) { + throw RabbitExceptionTranslator.convertRabbitAccessException(ex); + } } private void setupConfirm(Channel channel, Message message, @Nullable CorrelationData correlationDataArg) { - if ((this.publisherConfirms || this.confirmCallback != null) && channel instanceof PublisherCallbackChannel) { + final boolean publisherConfirms = channel instanceof ChannelProxy proxy + && proxy.isPublisherConfirms(); + + if ((publisherConfirms || this.confirmCallback != null) + && channel instanceof PublisherCallbackChannel publisherCallbackChannel) { - PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel; - CorrelationData correlationData = this.correlationDataPostProcessor != null - ? this.correlationDataPostProcessor.postProcess(message, correlationDataArg) - : correlationDataArg; long nextPublishSeqNo = channel.getNextPublishSeqNo(); - message.getMessageProperties().setPublishSequenceNumber(nextPublishSeqNo); - publisherCallbackChannel.addPendingConfirm(this, nextPublishSeqNo, - new PendingConfirm(correlationData, System.currentTimeMillis())); - if (correlationData != null && StringUtils.hasText(correlationData.getId())) { - message.getMessageProperties().setHeader(PublisherCallbackChannel.RETURNED_MESSAGE_CORRELATION_KEY, - correlationData.getId()); + if (nextPublishSeqNo > 0) { + CorrelationData correlationData = + this.correlationDataPostProcessor != null + ? this.correlationDataPostProcessor.postProcess(message, correlationDataArg) + : correlationDataArg; + message.getMessageProperties().setPublishSequenceNumber(nextPublishSeqNo); + publisherCallbackChannel.addPendingConfirm(this, nextPublishSeqNo, + new PendingConfirm(correlationData, System.currentTimeMillis())); + if (correlationData != null && StringUtils.hasText(correlationData.getId())) { + message.getMessageProperties().setHeader(PublisherCallbackChannel.RETURNED_MESSAGE_CORRELATION_KEY, + correlationData.getId()); + } + } + else { + logger.debug("Factory does not have confirms enabled"); } } - else if (channel instanceof ChannelProxy && ((ChannelProxy) channel).isConfirmSelected()) { + else if (channel instanceof ChannelProxy proxy && proxy.isConfirmSelected()) { long nextPublishSeqNo = channel.getNextPublishSeqNo(); message.getMessageProperties().setPublishSequenceNumber(nextPublishSeqNo); } @@ -2480,17 +2541,8 @@ private Message buildMessage(Envelope envelope, BasicProperties properties, byte return message; } - private MessageConverter getRequiredMessageConverter() throws IllegalStateException { - MessageConverter converter = getMessageConverter(); - if (converter == null) { - throw new AmqpIllegalStateException( - "No 'messageConverter' specified. Check configuration of RabbitTemplate."); - } - return converter; - } - private SmartMessageConverter getRequiredSmartMessageConverter() throws IllegalStateException { - MessageConverter converter = getRequiredMessageConverter(); + MessageConverter converter = getMessageConverter(); Assert.state(converter instanceof SmartMessageConverter, "template's message converter must be a SmartMessageConverter"); return (SmartMessageConverter) converter; @@ -2498,9 +2550,7 @@ private SmartMessageConverter getRequiredSmartMessageConverter() throws IllegalS private String getRequiredQueue() throws IllegalStateException { String name = this.defaultReceiveQueue; - if (name == null) { - throw new AmqpIllegalStateException("No 'queue' specified. Check configuration of RabbitTemplate."); - } + Assert.state(name != null, "No 'queue' specified. Check configuration of RabbitTemplate."); return name; } @@ -2520,12 +2570,6 @@ private String getRequiredQueue() throws IllegalStateException { private Address getReplyToAddress(Message request) throws AmqpException { Address replyTo = request.getMessageProperties().getReplyToAddress(); if (replyTo == null) { - if (this.exchange == null) { - throw new AmqpException( - "Cannot determine ReplyTo message property value: " - + "Request message does not contain reply-to property, " + - "and no default Exchange was set."); - } replyTo = new Address(this.exchange, this.routingKey); } return replyTo; @@ -2537,9 +2581,8 @@ private Address getReplyToAddress(Message request) throws AmqpException { * @since 2.0 */ public void addListener(Channel channel) { - if (channel instanceof PublisherCallbackChannel) { - PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel; - Channel key = channel instanceof ChannelProxy ? ((ChannelProxy) channel).getTargetChannel() : channel; + if (channel instanceof PublisherCallbackChannel publisherCallbackChannel) { + Channel key = channel instanceof ChannelProxy proxy ? proxy.getTargetChannel() : channel; if (this.publisherConfirmChannels.putIfAbsent(key, this) == null) { publisherCallbackChannel.addListener(this); if (logger.isDebugEnabled()) { @@ -2558,23 +2601,10 @@ public void addListener(Channel channel) { @Override public void handleConfirm(PendingConfirm pendingConfirm, boolean ack) { if (this.confirmCallback != null) { - this.confirmCallback - .confirm(pendingConfirm.getCorrelationData(), ack, pendingConfirm.getCause()); // NOSONAR never null + this.confirmCallback.confirm(pendingConfirm.getCorrelationData(), ack, pendingConfirm.getCause()); } } - @Override - @SuppressWarnings("deprecation") - public void handleReturn(int replyCode, - String replyText, - String exchange, - String routingKey, - BasicProperties properties, - byte[] body) { - - handleReturn(new Return(replyCode, replyText, exchange, routingKey, properties, body)); - } - @Override public void handleReturn(Return returned) { ReturnsCallback callback = this.returnsCallback; @@ -2662,17 +2692,6 @@ public void onMessage(Message message, @Nullable Channel channel) { } } - /** - * {@inheritDoc} - * - * @deprecated - use {@link #onMessage(Message, Channel)}. - */ - @Deprecated - @Override - public void onMessage(Message message) { - onMessage(message, null); - } - private void restoreProperties(Message message, PendingReply pendingReply) { if (!this.userCorrelationId) { // Restore the inbound correlation data @@ -2722,10 +2741,10 @@ public void handleDelivery(String consumerTag, Envelope envelope, BasicPropertie } }; - channel.basicConsume(queueName, consumer); + channel.basicConsume(queueName, false, this.consumerArgs, consumer); if (!latch.await(timeoutMillis, TimeUnit.MILLISECONDS)) { - if (channel instanceof ChannelProxy) { - ((ChannelProxy) channel).getTargetChannel().close(); + if (channel instanceof ChannelProxy proxy) { + proxy.getTargetChannel().close(); } future.completeExceptionally( new ConsumeOkNotReceivedException("Blocking receive, consumer failed to consume within " @@ -2735,18 +2754,15 @@ public void handleDelivery(String consumerTag, Envelope envelope, BasicPropertie return consumer; } - private static class PendingReply { + private static final class PendingReply { - @Nullable - private volatile String savedReplyTo; + private volatile @Nullable String savedReplyTo; - @Nullable - private volatile String savedCorrelation; + private volatile @Nullable String savedCorrelation; private final CompletableFuture future = new CompletableFuture<>(); - @Nullable - public String getSavedReplyTo() { + public @Nullable String getSavedReplyTo() { return this.savedReplyTo; } @@ -2754,8 +2770,7 @@ public void setSavedReplyTo(@Nullable String savedReplyTo) { this.savedReplyTo = savedReplyTo; } - @Nullable - public String getSavedCorrelation() { + public @Nullable String getSavedCorrelation() { return this.savedCorrelation; } @@ -2772,8 +2787,7 @@ public Message get() throws InterruptedException { } } - @Nullable - public Message get(long timeout, TimeUnit unit) throws InterruptedException { + public @Nullable Message get(long timeout, TimeUnit unit) throws InterruptedException { try { return this.future.get(timeout, unit); } @@ -2833,82 +2847,20 @@ public interface ConfirmCallback { } - /** - * A callback for returned messages. - * - * @deprecated in favor of {@link #returnedMessage(ReturnedMessage)} which is - * easier to use with lambdas. - */ - @Deprecated - @FunctionalInterface - public interface ReturnCallback { - - /** - * Returned message callback. - * @param message the returned message. - * @param replyCode the reply code. - * @param replyText the reply text. - * @param exchange the exchange. - * @param routingKey the routing key. - */ - void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey); - - /** - * Returned message callback. - * @param returned the returned message and metadata. - */ - @SuppressWarnings("deprecation") - default void returnedMessage(ReturnedMessage returned) { - returnedMessage(returned.getMessage(), returned.getReplyCode(), returned.getReplyText(), - returned.getExchange(), returned.getRoutingKey()); - } - - } - /** * A callback for returned messages. * * @since 2.3 */ @FunctionalInterface - public interface ReturnsCallback extends ReturnCallback { - - /** - * Returned message callback. - * @param message the returned message. - * @param replyCode the reply code. - * @param replyText the reply text. - * @param exchange the exchange. - * @param routingKey the routing key. - * @deprecated in favor of {@link #returnedMessage(ReturnedMessage)} which is - * easier to use with lambdas. - */ - @Override - @Deprecated - default void returnedMessage(Message message, int replyCode, String replyText, String exchange, - String routingKey) { - - throw new UnsupportedOperationException( - "This should never be called, please open a GitHub issue with a stack trace"); - }; + public interface ReturnsCallback { /** * Returned message callback. * @param returned the returned message and metadata. */ - @Override void returnedMessage(ReturnedMessage returned); - /** - * Internal use only; transitional during deprecation. - * @return the legacy delegate. - * @deprecated - will be removed with {@link ReturnCallback}. - */ - @Deprecated - @Nullable - default ReturnCallback delegate() { - return null; - } - } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/package-info.java index c82cc7b544..8cc9254744 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/package-info.java @@ -1,5 +1,5 @@ /** * Provides core classes for Spring Rabbit. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.core; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 1483a7cbaa..130428dd4e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,20 +21,26 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import java.util.Objects; import java.util.Properties; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import com.rabbitmq.client.Channel; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; import org.aopalliance.aop.Advice; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpIOException; @@ -43,6 +49,7 @@ import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.BatchMessageListener; +import org.springframework.amqp.core.Declarables; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessagePostProcessor; @@ -52,7 +59,6 @@ import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils; -import org.springframework.amqp.rabbit.connection.RabbitAccessor; import org.springframework.amqp.rabbit.connection.RabbitResourceHolder; import org.springframework.amqp.rabbit.connection.RabbitUtils; import org.springframework.amqp.rabbit.connection.RoutingConnectionFactory; @@ -65,38 +71,31 @@ import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation.DefaultRabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageReceiverContext; import org.springframework.amqp.support.ConditionalExceptionLogger; import org.springframework.amqp.support.ConsumerTagStrategy; import org.springframework.amqp.support.postprocessor.MessagePostProcessorUtils; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanNameAware; -import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.ErrorHandler; import org.springframework.util.StringUtils; import org.springframework.util.backoff.BackOff; import org.springframework.util.backoff.FixedBackOff; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ShutdownSignalException; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.Timer.Builder; -import io.micrometer.core.instrument.Timer.Sample; - /** * @author Mark Pollack * @author Mark Fisher @@ -108,10 +107,10 @@ * @author Arnaud Cogoluègnes * @author Artem Bilan * @author Mohammad Hewedy + * @author Mat Jaggard */ -public abstract class AbstractMessageListenerContainer extends RabbitAccessor - implements MessageListenerContainer, ApplicationContextAware, BeanNameAware, DisposableBean, - ApplicationEventPublisherAware { +public abstract class AbstractMessageListenerContainer extends ObservableListenerContainer + implements ApplicationEventPublisherAware { private static final int EXIT_99 = 99; @@ -130,33 +129,28 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor public static final long DEFAULT_SHUTDOWN_TIMEOUT = 5000; - private static final boolean MICROMETER_PRESENT = ClassUtils.isPresent( - "io.micrometer.core.instrument.MeterRegistry", AbstractMessageListenerContainer.class.getClassLoader()); - - private final Object lifecycleMonitor = new Object(); + protected final Lock lifecycleLock = new ReentrantLock(); private final ContainerDelegate delegate = this::actualInvokeListener; - protected final Object consumersMonitor = new Object(); //NOSONAR + protected final Lock consumersLock = new ReentrantLock(); //NOSONAR private final Map consumerArgs = new HashMap<>(); - private final Map micrometerTags = new HashMap<>(); + private final AtomicBoolean logDeclarationException = new AtomicBoolean(true); + + protected final AtomicBoolean stopNow = new AtomicBoolean(); // NOSONAR private ContainerDelegate proxy = this.delegate; private long shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; - @Nullable - private PlatformTransactionManager transactionManager; + private @Nullable PlatformTransactionManager transactionManager; private TransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); - @Nullable - private String beanName; - private Executor taskExecutor = new SimpleAsyncTaskExecutor(); private boolean taskExecutorSet; @@ -165,7 +159,7 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private MessagePropertiesConverter messagePropertiesConverter = new DefaultMessagePropertiesConverter(); - private AmqpAdmin amqpAdmin; + private @Nullable AmqpAdmin amqpAdmin; private boolean missingQueuesFatal = true; @@ -185,15 +179,15 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private int phase = Integer.MAX_VALUE; - private boolean active = false; + private volatile boolean active = false; - private boolean running = false; + private volatile boolean running = false; private ErrorHandler errorHandler = new ConditionalRejectingErrorHandler(); private boolean exposeListenerChannel = true; - private MessageListener messageListener; + private @Nullable MessageListener messageListener; private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; @@ -201,16 +195,11 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private boolean initialized; - private Collection afterReceivePostProcessors; - - private ApplicationContext applicationContext; - - private String listenerId; + private @Nullable Collection afterReceivePostProcessors; private Advice[] adviceChain = new Advice[0]; - @Nullable - private ConsumerTagStrategy consumerTagStrategy; + private @Nullable ConsumerTagStrategy consumerTagStrategy; private boolean exclusive; @@ -240,10 +229,6 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private BatchingStrategy batchingStrategy = new SimpleBatchingStrategy(0, 0, 0L); - private MicrometerHolder micrometerHolder; - - private boolean micrometerEnabled = true; - private boolean isBatchListener; private long consumeDelay; @@ -254,12 +239,21 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private volatile boolean lazyLoad; + private boolean asyncReplies; + + private MessageAckListener messageAckListener = (success, deliveryTag, cause) -> { + }; + + private @Nullable RabbitListenerObservationConvention observationConvention; + + private boolean forceStop; + @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } - protected ApplicationEventPublisher getApplicationEventPublisher() { + protected @Nullable ApplicationEventPublisher getApplicationEventPublisher() { return this.applicationEventPublisher; } @@ -344,7 +338,7 @@ protected Map getQueueNamesToQueues() { private List queuesToNames() { return this.queues.stream() .map(Queue::getActualName) - .collect(Collectors.toList()); + .toList(); } /** @@ -382,7 +376,7 @@ public void addQueues(Queue... queues) { public boolean removeQueueNames(String... queueNames) { Assert.notNull(queueNames, "'queueNames' cannot be null"); Assert.noNullElements(queueNames, "'queueNames' cannot contain null elements"); - if (this.queues.size() > 0) { + if (!this.queues.isEmpty()) { Set toRemove = new HashSet<>(Arrays.asList(queueNames)); return this.queues.removeIf( q -> toRemove.contains(q.getActualName())); @@ -438,6 +432,7 @@ public void setMessageListener(MessageListener messageListener) { this.messageListener = messageListener; this.isBatchListener = messageListener instanceof BatchMessageListener || messageListener instanceof ChannelAwareBatchMessageListener; + this.asyncReplies = messageListener.isAsyncReplies(); } /** @@ -455,16 +450,18 @@ protected void checkMessageListener(Object listener) { } } + /** + * Get a reference to the message listener. + * @return the message listener. + */ @Override - @Nullable - public Object getMessageListener() { + public @Nullable MessageListener getMessageListener() { return this.messageListener; } /** * Set an ErrorHandler to be invoked in case of any uncaught exceptions thrown while processing a Message. By - * default a {@link ConditionalRejectingErrorHandler} with its default list of fatal exceptions will be used. - * + * default, a {@link ConditionalRejectingErrorHandler} with its default list of fatal exceptions will be used. * @param errorHandler The error handler. */ public void setErrorHandler(ErrorHandler errorHandler) { @@ -472,7 +469,16 @@ public void setErrorHandler(ErrorHandler errorHandler) { } /** - * Determine whether or not the container should de-batch batched + * Return the {@link ErrorHandler}. + * @return the {@link ErrorHandler} + * @since 3.1.9 + */ + public ErrorHandler getErrorHandler() { + return this.errorHandler; + } + + /** + * Determine whether the container should de-batch batched * messages (true) or call the listener with the batch (false). Default: true. * @param deBatchingEnabled the deBatchingEnabled to set. * @see #setBatchingStrategy(BatchingStrategy) @@ -554,7 +560,6 @@ public boolean removeAfterReceivePostProcessor(MessagePostProcessor afterReceive * Set whether to automatically start the container after initialization. *

* Default is "true"; set this to "false" to allow for manual startup through the {@link #start()} method. - * * @param autoStartup true for auto startup. */ @Override @@ -571,7 +576,6 @@ public boolean isAutoStartup() { * Specify the phase in which this container should be started and stopped. The startup order proceeds from lowest * to highest, and the shutdown order is the reverse of that. By default this value is Integer.MAX_VALUE meaning * that this container starts as late as possible and stops as soon as possible. - * * @param phase The phase. */ public void setPhase(int phase) { @@ -586,34 +590,12 @@ public int getPhase() { return this.phase; } - @Override - public void setBeanName(String beanName) { - this.beanName = beanName; - } - - /** - * @return The bean name that this listener container has been assigned in its containing bean factory, if any. - */ - @Nullable - protected final String getBeanName() { - return this.beanName; - } - - protected final ApplicationContext getApplicationContext() { - return this.applicationContext; - } - - @Override - public final void setApplicationContext(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - @Override public ConnectionFactory getConnectionFactory() { ConnectionFactory connectionFactory = super.getConnectionFactory(); - if (connectionFactory instanceof RoutingConnectionFactory) { - ConnectionFactory targetConnectionFactory = ((RoutingConnectionFactory) connectionFactory) - .getTargetConnectionFactory(getRoutingLookupKey()); // NOSONAR never null + if (connectionFactory instanceof RoutingConnectionFactory rcf) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + ConnectionFactory targetConnectionFactory = rcf.getTargetConnectionFactory(getRoutingLookupKey()); if (targetConnectionFactory != null) { return targetConnectionFactory; } @@ -659,8 +641,7 @@ public void setForceCloseChannel(boolean forceCloseChannel) { * @since 1.6.9 * @see #setLookupKeyQualifier(String) */ - @Nullable - protected String getRoutingLookupKey() { + protected @Nullable String getRoutingLookupKey() { return super.getConnectionFactory() instanceof RoutingConnectionFactory ? this.lookupKeyQualifier + queuesAsListString() : null; @@ -673,30 +654,13 @@ private String queuesAsListString() { } /** - * Return the (@link RoutingConnectionFactory} if the connection factory is a + * Return the {@link RoutingConnectionFactory} if the connection factory is a * {@link RoutingConnectionFactory}; null otherwise. * @return the {@link RoutingConnectionFactory} or null. * @since 1.6.9 */ - @Nullable - protected RoutingConnectionFactory getRoutingConnectionFactory() { - return super.getConnectionFactory() instanceof RoutingConnectionFactory - ? (RoutingConnectionFactory) super.getConnectionFactory() - : null; - } - - /** - * The 'id' attribute of the listener. - * @return the id (or the container bean name if no id set). - */ - @Nullable - public String getListenerId() { - return this.listenerId != null ? this.listenerId : this.beanName; - } - - @Override - public void setListenerId(String listenerId) { - this.listenerId = listenerId; + protected @Nullable RoutingConnectionFactory getRoutingConnectionFactory() { + return super.getConnectionFactory() instanceof RoutingConnectionFactory rcf ? rcf : null; } /** @@ -714,8 +678,7 @@ public void setConsumerTagStrategy(ConsumerTagStrategy consumerTagStrategy) { * @return the strategy. * @since 2.0 */ - @Nullable - protected ConsumerTagStrategy getConsumerTagStrategy() { + protected @Nullable ConsumerTagStrategy getConsumerTagStrategy() { return this.consumerTagStrategy; } @@ -725,10 +688,14 @@ protected ConsumerTagStrategy getConsumerTagStrategy() { * @since 1.3 */ public void setConsumerArguments(Map args) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { this.consumerArgs.clear(); this.consumerArgs.putAll(args); } + finally { + this.consumersLock.unlock(); + } } /** @@ -737,9 +704,13 @@ public void setConsumerArguments(Map args) { * @since 2.0 */ public Map getConsumerArguments() { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { return new HashMap<>(this.consumerArgs); } + finally { + this.consumersLock.unlock(); + } } /** @@ -781,7 +752,7 @@ protected boolean isNoLocal() { * to be sent to the dead letter exchange. Setting to false causes all rejections to not * be requeued. When true, the default can be overridden by the listener throwing an * {@link AmqpRejectAndDontRequeueException}. Default true. - * @param defaultRequeueRejected true to reject by default. + * @param defaultRequeueRejected true to requeue by default. */ public void setDefaultRequeueRejected(boolean defaultRequeueRejected) { this.defaultRequeueRejected = defaultRequeueRejected; @@ -874,8 +845,7 @@ public void setTransactionManager(PlatformTransactionManager transactionManager) this.transactionManager = transactionManager; } - @Nullable - protected PlatformTransactionManager getTransactionManager() { + protected @Nullable PlatformTransactionManager getTransactionManager() { return this.transactionManager; } @@ -946,14 +916,13 @@ protected MessagePropertiesConverter getMessagePropertiesConverter() { return this.messagePropertiesConverter; } - @Nullable - protected AmqpAdmin getAmqpAdmin() { + protected @Nullable AmqpAdmin getAmqpAdmin() { return this.amqpAdmin; } /** * Set the {@link AmqpAdmin}, used to declare any auto-delete queues, bindings - * etc when the container is started. Only needed if those queues use conditional + * etc. when the container is started. Only needed if those queues use conditional * declaration (have a 'declared-by' attribute). If not specified, an internal * admin will be used which will attempt to declare all elements not having a * 'declared-by' attribute. @@ -965,7 +934,7 @@ public void setAmqpAdmin(AmqpAdmin amqpAdmin) { } /** - * If all of the configured queue(s) are not available on the broker, this setting + * If all the configured queue(s) are not available on the broker, this setting * determines whether the condition is fatal. When true, and * the queues are missing during startup, the context refresh() will fail. *

When false, the condition is not considered fatal and the container will @@ -989,7 +958,7 @@ protected boolean isMissingQueuesFatalSet() { /** * Prevent the container from starting if any of the queues defined in the context have - * mismatched arguments (TTL etc). Default false. + * mismatched arguments (TTL etc.). Default false. * @param mismatchedQueuesFatal true to fail initialization when this condition occurs. * @since 1.6 */ @@ -1001,7 +970,6 @@ protected boolean isMismatchedQueuesFatal() { return this.mismatchedQueuesFatal; } - public void setPossibleAuthenticationFailureFatal(boolean possibleAuthenticationFailureFatal) { doSetPossibleAuthenticationFailureFatal(possibleAuthenticationFailureFatal); this.possibleAuthenticationFailureFatalSet = true; @@ -1015,11 +983,14 @@ public boolean isPossibleAuthenticationFailureFatal() { return this.possibleAuthenticationFailureFatal; } - protected boolean isPossibleAuthenticationFailureFatalSet() { return this.possibleAuthenticationFailureFatalSet; } + protected boolean isAsyncReplies() { + return this.asyncReplies; + } + /** * Set to true to automatically declare elements (queues, exchanges, bindings) * in the application context during container start(). @@ -1066,7 +1037,7 @@ public void setStatefulRetryFatalWithNullMessageId(boolean statefulRetryFatalWit /** * Set a {@link ConditionalExceptionLogger} for logging exclusive consumer failures. The - * default is to log such failures at WARN level. + * default is to log such failures at DEBUG level (since 3.1, previously WARN). * @param exclusiveConsumerExceptionLogger the conditional exception logger. * @since 1.5 */ @@ -1128,28 +1099,17 @@ protected BatchingStrategy getBatchingStrategy() { return this.batchingStrategy; } - protected Collection getAfterReceivePostProcessors() { + protected @Nullable Collection getAfterReceivePostProcessors() { return this.afterReceivePostProcessors; } /** - * Set additional tags for the Micrometer listener timers. - * @param tags the tags. - * @since 2.2 + * Set an observation convention; used to add additional key/values to observations. + * @param observationConvention the convention. + * @since 3.0 */ - public void setMicrometerTags(Map tags) { - if (tags != null) { - this.micrometerTags.putAll(tags); - } - } - - /** - * Set to false to disable micrometer listener timers. - * @param micrometerEnabled false to disable. - * @since 2.2 - */ - public void setMicrometerEnabled(boolean micrometerEnabled) { - this.micrometerEnabled = micrometerEnabled; + public void setObservationConvention(RabbitListenerObservationConvention observationConvention) { + this.observationConvention = observationConvention; } /** @@ -1181,12 +1141,66 @@ protected JavaLangErrorHandler getJavaLangErrorHandler() { * is called. * @param javaLangErrorHandler the handler. * @since 2.2.12 + * @deprecated in favor of {@link #setJavaLangErrorHandler(JavaLangErrorHandler)} */ + @Deprecated(since = "4.0.0", forRemoval = true) public void setjavaLangErrorHandler(JavaLangErrorHandler javaLangErrorHandler) { Assert.notNull(javaLangErrorHandler, "'javaLangErrorHandler' cannot be null"); this.javaLangErrorHandler = javaLangErrorHandler; } + /** + * Provide a JavaLangErrorHandler implementation; by default, {@code System.exit(99)} + * is called. + * @param javaLangErrorHandler the handler. + * @since 4.0.0 + */ + public void setJavaLangErrorHandler(JavaLangErrorHandler javaLangErrorHandler) { + Assert.notNull(javaLangErrorHandler, "'javaLangErrorHandler' cannot be null"); + this.javaLangErrorHandler = javaLangErrorHandler; + } + + /** + * Set a {@link MessageAckListener} to use when ack a message(messages) in + * {@link AcknowledgeMode#AUTO} mode. + * @param messageAckListener the messageAckListener. + * @since 2.4.6 + * @see MessageAckListener + * @see AcknowledgeMode + */ + public void setMessageAckListener(MessageAckListener messageAckListener) { + Assert.notNull(messageAckListener, "'messageAckListener' cannot be null"); + this.messageAckListener = messageAckListener; + } + + /** + * Return the {@link MessageAckListener}. + * @return the {@link MessageAckListener} + * @since 3.1.9 + */ + public MessageAckListener getMessageAckListener() { + return this.messageAckListener; + } + + /** + * Stop container after current message(s) are processed and requeue any prefetched. + * @return true to stop when current message(s) are processed. + * @since 2.4.14 + */ + protected boolean isForceStop() { + return this.forceStop; + } + + /** + * Set to true to stop the container after the current message(s) are processed and + * requeue any prefetched. Useful when using exclusive or single-active consumers. + * @param forceStop true to stop when current message(s) are processed. + * @since 2.4.14 + */ + public void setForceStop(boolean forceStop) { + this.forceStop = forceStop; + } + /** * Delegates to {@link #validateConfiguration()} and {@link #initialize()}. */ @@ -1205,19 +1219,9 @@ public void afterPropertiesSet() { "channelTransacted=false"); validateConfiguration(); initialize(); - try { - if (this.micrometerHolder == null && MICROMETER_PRESENT && this.micrometerEnabled - && this.applicationContext != null) { - String id = getListenerId(); - if (id == null) { - id = "no_id_or_beanName"; - } - this.micrometerHolder = new MicrometerHolder(this.applicationContext, id, - this.micrometerTags); - } - } - catch (IllegalStateException e) { - this.logger.debug("Could not enable micrometer timers", e); + checkMicrometer(); + if (this.isAsyncReplies() && !AcknowledgeMode.MANUAL.equals(this.acknowledgeMode)) { + this.acknowledgeMode = AcknowledgeMode.MANUAL; } } @@ -1254,9 +1258,7 @@ protected void initializeProxy(Object delegate) { @Override public void destroy() { shutdown(); - if (this.micrometerHolder != null) { - this.micrometerHolder.destroy(); - } + super.destroy(); } // ------------------------------------------------------------------------- @@ -1269,63 +1271,91 @@ public void destroy() { * Creates a Rabbit Connection and calls {@link #doInitialize()}. */ public void initialize() { - try { - synchronized (this.lifecycleMonitor) { - this.lifecycleMonitor.notifyAll(); - } - initializeProxy(this.delegate); - checkMissingQueuesFatalFromProperty(); - checkPossibleAuthenticationFailureFatalFromProperty(); - doInitialize(); - if (!this.isExposeListenerChannel() && this.transactionManager != null) { - logger.warn("exposeListenerChannel=false is ignored when using a TransactionManager"); - } - if (!this.taskExecutorSet && StringUtils.hasText(getListenerId())) { - this.taskExecutor = new SimpleAsyncTaskExecutor(getListenerId() + "-"); - this.taskExecutorSet = true; + if (!this.initialized) { + this.lifecycleLock.lock(); + try { + if (!this.initialized) { + initializeProxy(this.delegate); + checkMissingQueuesFatalFromProperty(); + checkPossibleAuthenticationFailureFatalFromProperty(); + doInitialize(); + if (!this.isExposeListenerChannel() && this.transactionManager != null) { + logger.warn("exposeListenerChannel=false is ignored when using a TransactionManager"); + } + if (!this.taskExecutorSet && StringUtils.hasText(getListenerId())) { + this.taskExecutor = new SimpleAsyncTaskExecutor(getListenerId() + "-"); + this.taskExecutorSet = true; + } + if (this.transactionManager != null && !isChannelTransacted()) { + logger.debug("The 'channelTransacted' is coerced to 'true', when 'transactionManager' is provided"); + setChannelTransacted(true); + } + if (this.messageListener != null) { + this.messageListener.containerAckMode(this.acknowledgeMode); + } + this.initialized = true; + } } - if (this.transactionManager != null && !isChannelTransacted()) { - logger.debug("The 'channelTransacted' is coerced to 'true', when 'transactionManager' is provided"); - setChannelTransacted(true); + catch (Exception ex) { + throw convertRabbitAccessException(ex); } - if (this.messageListener != null) { - this.messageListener.containerAckMode(this.acknowledgeMode); + finally { + this.lifecycleLock.unlock(); } - this.initialized = true; - } - catch (Exception ex) { - throw convertRabbitAccessException(ex); } } /** - * Stop the shared Connection, call {@link #doShutdown()}, and close this container. + * Stop the shared Connection, call {@link #shutdown(Runnable)}, and close this + * container. */ public void shutdown() { - synchronized (this.lifecycleMonitor) { + shutdown(null); + this.initialized = false; + } + + /** + * Stop the shared Connection, call {@link #shutdownAndWaitOrCallback(Runnable)}, and + * close this container. + * @param callback an optional {@link Runnable} to call when the stop is complete. + */ + public void shutdown(@Nullable Runnable callback) { + this.lifecycleLock.lock(); + try { if (!isActive()) { logger.debug("Shutdown ignored - container is not active already"); - this.lifecycleMonitor.notifyAll(); + if (callback != null) { + callback.run(); + } return; } this.active = false; - this.lifecycleMonitor.notifyAll(); + } + finally { + this.lifecycleLock.unlock(); } logger.debug("Shutting down Rabbit listener container"); // Shut down the invokers. try { - doShutdown(); + shutdownAndWaitOrCallback(callback); } catch (Exception ex) { throw convertRabbitAccessException(ex); } finally { - synchronized (this.lifecycleMonitor) { - this.running = false; - this.lifecycleMonitor.notifyAll(); - } + setNotRunning(); + } + } + + protected void setNotRunning() { + this.lifecycleLock.lock(); + try { + this.running = false; + } + finally { + this.lifecycleLock.unlock(); } } @@ -1344,15 +1374,23 @@ public void shutdown() { * A shared Rabbit Connection, if any, will automatically be closed afterwards. * @see #shutdown() */ - protected abstract void doShutdown(); + protected void doShutdown() { + shutdownAndWaitOrCallback(null); + } + + @Override + public void stop(Runnable callback) { + shutdown(callback); + } + + protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { + } /** * @return Whether this container is currently active, that is, whether it has been set up but not shut down yet. */ public final boolean isActive() { - synchronized (this.lifecycleMonitor) { - return this.active; - } + return this.active; } /** @@ -1365,12 +1403,17 @@ public void start() { return; } if (!this.initialized) { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { if (!this.initialized) { afterPropertiesSet(); } } + finally { + this.lifecycleLock.unlock(); + } } + checkObservation(); try { logger.debug("Starting Rabbit listener container."); configureAdminIfNeeded(); @@ -1390,10 +1433,13 @@ public void start() { */ protected void doStart() { // Reschedule paused tasks, if any. - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.active = true; this.running = true; - this.lifecycleMonitor.notifyAll(); + } + finally { + this.lifecycleLock.unlock(); } } @@ -1411,20 +1457,7 @@ public void stop() { throw convertRabbitAccessException(ex); } finally { - synchronized (this.lifecycleMonitor) { - this.running = false; - this.lifecycleMonitor.notifyAll(); - } - } - } - - @Override - public void stop(Runnable callback) { - try { - stop(); - } - finally { - callback.run(); + setNotRunning(); } } @@ -1442,9 +1475,7 @@ protected void doStop() { */ @Override public final boolean isRunning() { - synchronized (this.lifecycleMonitor) { - return (this.running); - } + return this.running; } /** @@ -1455,18 +1486,13 @@ public final boolean isRunning() { * @see #setErrorHandler */ protected void invokeErrorHandler(Throwable ex) { - if (this.errorHandler != null) { - try { - this.errorHandler.handleError(ex); - } - catch (Exception e) { - LogFactory.getLog(this.errorHandlerLoggerName).error( - "Execution of Rabbit message listener failed, and the error handler threw an exception", e); - throw e; - } + try { + this.errorHandler.handleError(ex); } - else { - logger.warn("Execution of Rabbit message listener failed, and no ErrorHandler has been set.", ex); + catch (Exception e) { + LogFactory.getLog(this.errorHandlerLoggerName).error( + "Execution of Rabbit message listener failed, and the error handler threw an exception", e); + throw e; } } @@ -1475,14 +1501,28 @@ protected void invokeErrorHandler(Throwable ex) { // ------------------------------------------------------------------------- /** - * Execute the specified listener, committing or rolling back the transaction afterwards (if necessary). + * Execute the specified listener, committing or rolling back the transaction afterward (if necessary). * @param channel the Rabbit Channel to operate on * @param data the received Rabbit Message * @see #invokeListener * @see #handleListenerException */ - @SuppressWarnings(UNCHECKED) protected void executeListener(Channel channel, Object data) { + Observation observation; + ObservationRegistry registry = getObservationRegistry(); + if (data instanceof Message message) { + observation = RabbitListenerObservation.LISTENER_OBSERVATION.observation(this.observationConvention, + DefaultRabbitListenerObservationConvention.INSTANCE, + () -> new RabbitMessageReceiverContext(message, getListenerId()), registry); + observation.observe(() -> executeListenerAndHandleException(channel, data)); + } + else { + executeListenerAndHandleException(channel, data); + } + } + + @SuppressWarnings(UNCHECKED) + protected void executeListenerAndHandleException(Channel channel, Object data) { if (!isRunning()) { if (logger.isWarnEnabled()) { logger.warn( @@ -1491,26 +1531,27 @@ protected void executeListener(Channel channel, Object data) { throw new MessageRejectedWhileStoppingException(); } Object sample = null; - if (this.micrometerHolder != null) { - sample = this.micrometerHolder.start(); + MicrometerHolder micrometerHolder = getMicrometerHolder(); + if (micrometerHolder != null) { + sample = micrometerHolder.start(); } try { doExecuteListener(channel, data); - if (sample != null) { - this.micrometerHolder.success(sample, data instanceof Message - ? ((Message) data).getMessageProperties().getConsumerQueue() + if (micrometerHolder != null && sample != null) { + micrometerHolder.success(sample, data instanceof Message message + ? Objects.requireNonNull(message.getMessageProperties().getConsumerQueue()) : queuesAsListString()); } } catch (RuntimeException ex) { - if (sample != null) { - this.micrometerHolder.failure(sample, data instanceof Message - ? ((Message) data).getMessageProperties().getConsumerQueue() + if (micrometerHolder != null && sample != null) { + micrometerHolder.failure(sample, data instanceof Message message + ? Objects.requireNonNull(message.getMessageProperties().getConsumerQueue()) : queuesAsListString(), ex.getClass().getSimpleName()); } Message message; - if (data instanceof Message) { - message = (Message) data; + if (data instanceof Message msg) { + message = msg; } else { message = ((List) data).get(0); @@ -1536,15 +1577,10 @@ private void checkStatefulRetry(RuntimeException ex, Message message) { } private void doExecuteListener(Channel channel, Object data) { - if (data instanceof Message) { - Message message = (Message) data; + if (data instanceof Message message) { if (this.afterReceivePostProcessors != null) { for (MessagePostProcessor processor : this.afterReceivePostProcessors) { message = processor.postProcessMessage(message); - if (message == null) { - throw new ImmediateAcknowledgeAmqpException( - "Message Post Processor returned 'null', discarding message"); - } } } if (this.deBatchingEnabled && this.batchingStrategy.canDebatch(message.getMessageProperties())) { @@ -1570,35 +1606,28 @@ protected void invokeListener(Channel channel, Object data) { * @see #setMessageListener(MessageListener) */ protected void actualInvokeListener(Channel channel, Object data) { - Object listener = getMessageListener(); - if (listener instanceof ChannelAwareMessageListener) { - doInvokeListener((ChannelAwareMessageListener) listener, channel, data); + MessageListener listener = getMessageListener(); + Assert.notNull(listener, "listener cannot be null"); + if (listener instanceof ChannelAwareMessageListener chaml) { + doInvokeListener(chaml, channel, data); } - else if (listener instanceof MessageListener) { + else { boolean bindChannel = isExposeListenerChannel() && isChannelLocallyTransacted(); if (bindChannel) { RabbitResourceHolder resourceHolder = new RabbitResourceHolder(channel, false); resourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.bindResource(this.getConnectionFactory(), - resourceHolder); + TransactionSynchronizationManager.bindResource(getConnectionFactory(), resourceHolder); } try { - doInvokeListener((MessageListener) listener, data); + doInvokeListener(listener, data); } finally { if (bindChannel) { // unbind if we bound - TransactionSynchronizationManager.unbindResource(this.getConnectionFactory()); + TransactionSynchronizationManager.unbindResource(getConnectionFactory()); } } } - else if (listener != null) { - throw new FatalListenerExecutionException("Only MessageListener and SessionAwareMessageListener supported: " - + listener); - } - else { - throw new FatalListenerExecutionException("No message listener specified - see property 'messageListener'"); - } } /** @@ -1611,10 +1640,10 @@ else if (listener != null) { * @see ChannelAwareMessageListener * @see #setExposeListenerChannel(boolean) */ - @SuppressWarnings(UNCHECKED) + @SuppressWarnings({UNCHECKED, "NullAway"}) // Dataflow analysis limitation protected void doInvokeListener(ChannelAwareMessageListener listener, Channel channel, Object data) { - Message message = null; + Message message; RabbitResourceHolder resourceHolder = null; Channel channelToUse = channel; boolean boundHere = false; @@ -1626,13 +1655,12 @@ protected void doInvokeListener(ChannelAwareMessageListener listener, Channel ch /* * If there is a real transaction, the resource will have been bound; otherwise * we need to bind it temporarily here. Any work done on this channel - * will be committed in the finally block. + * will be committed in the {@code finally} block. */ if (isChannelLocallyTransacted() && !TransactionSynchronizationManager.isActualTransactionActive()) { resourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.bindResource(this.getConnectionFactory(), - resourceHolder); + TransactionSynchronizationManager.bindResource(getConnectionFactory(), resourceHolder); boundHere = true; } } @@ -1641,8 +1669,7 @@ protected void doInvokeListener(ChannelAwareMessageListener listener, Channel ch if (isChannelLocallyTransacted()) { RabbitResourceHolder localResourceHolder = new RabbitResourceHolder(channelToUse, false); localResourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.bindResource(this.getConnectionFactory(), - localResourceHolder); + TransactionSynchronizationManager.bindResource(getConnectionFactory(), localResourceHolder); boundHere = true; } } @@ -1661,7 +1688,7 @@ protected void doInvokeListener(ChannelAwareMessageListener listener, Channel ch } } finally { - cleanUpAfterInvoke(resourceHolder, channelToUse, boundHere); // NOSONAR channel not null here + cleanUpAfterInvoke(resourceHolder, channelToUse, boundHere); } } @@ -1694,15 +1721,13 @@ private void cleanUpAfterInvoke(@Nullable RabbitResourceHolder resourceHolder, C * Default implementation performs a plain invocation of the onMessage method. *

* Exception thrown from listener will be wrapped to {@link ListenerExecutionFailedException}. - * * @param listener the Rabbit MessageListener to invoke * @param data the received Rabbit Message or List of Message. - * * @see org.springframework.amqp.core.MessageListener#onMessage */ @SuppressWarnings(UNCHECKED) protected void doInvokeListener(MessageListener listener, Object data) { - Message message = null; + Message message; try { if (data instanceof List) { listener.onMessageBatch((List) data); @@ -1773,7 +1798,7 @@ protected ListenerExecutionFailedException wrapToListenerExecutionFailedExceptio return (ListenerExecutionFailedException) e; } - protected void publishConsumerFailedEvent(String reason, boolean fatal, @Nullable Throwable t) { + protected void publishConsumerFailedEvent(@Nullable String reason, boolean fatal, @Nullable Throwable t) { if (this.applicationEventPublisher != null) { this.applicationEventPublisher .publishEvent(t == null ? new ListenerContainerConsumerTerminatedEvent(this, reason) : @@ -1802,8 +1827,11 @@ protected void updateLastReceive() { } protected void configureAdminIfNeeded() { - if (this.amqpAdmin == null && this.getApplicationContext() != null) { - Map admins = this.getApplicationContext().getBeansOfType(AmqpAdmin.class); + ApplicationContext applicationContext = getApplicationContext(); + if (this.amqpAdmin == null && applicationContext != null) { + Map admins = + BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, AmqpAdmin.class, + false, false); if (admins.size() == 1) { this.amqpAdmin = admins.values().iterator().next(); } @@ -1811,12 +1839,12 @@ protected void configureAdminIfNeeded() { if ((isAutoDeclare() || isMismatchedQueuesFatal()) && this.logger.isDebugEnabled()) { logger.debug("For 'autoDeclare' and 'mismatchedQueuesFatal' to work, there must be exactly one " + "AmqpAdmin in the context or you must inject one into this container; found: " - + admins.size() + " for container " + toString()); + + admins.size() + " for container " + this); } if (isMismatchedQueuesFatal()) { throw new IllegalStateException("When 'mismatchedQueuesFatal' is 'true', there must be exactly " + "one AmqpAdmin in the context or you must inject one into this container; found: " - + admins.size() + " for container " + toString()); + + admins.size() + " for container " + this); } } } @@ -1842,9 +1870,7 @@ protected void checkMismatchedQueues() { else { try { Connection connection = getConnectionFactory().createConnection(); // NOSONAR - if (connection != null) { - connection.close(); - } + connection.close(); } catch (Exception e) { logger.info("Broker not available; cannot force queue declarations during start: " + e.getMessage()); @@ -1879,7 +1905,7 @@ else if (this.missingQueuesFatal) { * Declaration is idempotent so, aside from some network chatter, there is no issue, * and we only will do it if we detect our queue is gone. *

- * In general it makes sense only for the 'auto-delete' or 'expired' queues, + * In general, it makes sense only for the 'auto-delete' or 'expired' queues, * but with the server TTL policy we don't have ability to determine 'expiration' * option for the queue. *

@@ -1888,28 +1914,47 @@ else if (this.missingQueuesFatal) { * the declarations are always attempted during restart so the listener will * fail with a fatal error if mismatches occur. */ - protected synchronized void redeclareElementsIfNecessary() { - AmqpAdmin admin = getAmqpAdmin(); - if (!this.lazyLoad && admin != null && isAutoDeclare()) { - try { - attemptDeclarations(admin); - } - catch (Exception e) { - if (RabbitUtils.isMismatchedQueueArgs(e)) { - throw new FatalListenerStartupException("Mismatched queues", e); + protected void redeclareElementsIfNecessary() { + this.lifecycleLock.lock(); + try { + AmqpAdmin admin = getAmqpAdmin(); + if (!this.lazyLoad && admin != null && isAutoDeclare()) { + try { + attemptDeclarations(admin); + this.logDeclarationException.set(true); + } + catch (Exception e) { + if (RabbitUtils.isMismatchedQueueArgs(e)) { + throw new FatalListenerStartupException("Mismatched queues", e); + } + if (this.logDeclarationException.getAndSet(false)) { + this.logger.error("Failed to check/redeclare auto-delete queue(s).", e); + } + else { + this.logger.error("Failed to check/redeclare auto-delete queue(s)."); + } } - logger.error("Failed to check/redeclare auto-delete queue(s).", e); } } + finally { + this.lifecycleLock.unlock(); + } } private void attemptDeclarations(AmqpAdmin admin) { - ApplicationContext context = this.getApplicationContext(); + ApplicationContext context = getApplicationContext(); if (context != null) { Set queueNames = getQueueNamesAsSet(); - Map queueBeans = context.getBeansOfType(Queue.class); - for (Entry entry : queueBeans.entrySet()) { - Queue queue = entry.getValue(); + Collection queues = new LinkedHashSet<>( + context.getBeansOfType(Queue.class, false, false).values()); + Map declarables = context.getBeansOfType(Declarables.class, false, false); + declarables.values().forEach(dec -> queues.addAll(dec.getDeclarablesByType(Queue.class))); + admin.getManualDeclarableSet() + .stream() + .filter(Queue.class::isInstance) + .map(Queue.class::cast) + .forEach(queues::add); + for (Queue queue : queues) { if (isMismatchedQueuesFatal() || (queueNames.contains(queue.getName()) && admin.getQueueProperties(queue.getName()) == null)) { if (logger.isDebugEnabled()) { @@ -1954,7 +1999,7 @@ else if (cause instanceof AmqpRejectAndDontRequeueException || cause instanceof * @param resourceHolder the bound resource holder (if a transaction is active). * @param exception the exception. */ - protected void prepareHolderForRollback(RabbitResourceHolder resourceHolder, RuntimeException exception) { + protected void prepareHolderForRollback(@Nullable RabbitResourceHolder resourceHolder, RuntimeException exception) { if (resourceHolder != null) { resourceHolder.setRequeueOnRollback(isAlwaysRequeueWithTxManagerRollback() || ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), exception, logger)); @@ -2009,7 +2054,7 @@ protected List debatch(Message message) { if (this.isBatchListener && isDeBatchingEnabled() && getBatchingStrategy().canDebatch(message.getMessageProperties())) { final List messageList = new ArrayList<>(); - getBatchingStrategy().deBatch(message, fragment -> messageList.add(fragment)); + getBatchingStrategy().deBatch(message, messageList::add); return messageList; } return null; @@ -2072,97 +2117,13 @@ protected WrappedTransactionException(Throwable cause) { * consumer failures. * @since 1.5 */ - private static class DefaultExclusiveConsumerLogger implements ConditionalExceptionLogger { - - DefaultExclusiveConsumerLogger() { - } + public static class DefaultExclusiveConsumerLogger implements ConditionalExceptionLogger { @Override - public void log(Log logger, String message, Throwable t) { - if (t instanceof ShutdownSignalException) { - ShutdownSignalException cause = (ShutdownSignalException) t; - if (RabbitUtils.isExclusiveUseChannelClose(cause)) { - if (logger.isWarnEnabled()) { - logger.warn(message + ": " + cause.toString()); - } - } - else if (!RabbitUtils.isNormalChannelClose(cause)) { - logger.error(message + ": " + cause.getMessage()); - } + public void log(Log logger, String message, @Nullable Throwable cause) { + if (logger.isDebugEnabled()) { + logger.debug(message + ": " + cause); } - else { - if (logger.isErrorEnabled()) { - logger.error("Unexpected invocation of " + getClass() + ", with message: " + message, t); - } - } - } - - } - - private static final class MicrometerHolder { - - private final ConcurrentMap timers = new ConcurrentHashMap<>(); - - private final MeterRegistry registry; - - private final Map tags; - - private final String listenerId; - - MicrometerHolder(@Nullable ApplicationContext context, String listenerId, Map tags) { - if (context == null) { - throw new IllegalStateException("No micrometer registry present"); - } - Map registries = context.getBeansOfType(MeterRegistry.class, false, false); - if (registries.size() == 1) { - this.registry = registries.values().iterator().next(); - this.listenerId = listenerId; - this.tags = tags; - } - else { - throw new IllegalStateException("No micrometer registry present"); - } - } - - Object start() { - return Timer.start(this.registry); - } - - void success(Object sample, String queue) { - Timer timer = this.timers.get(queue + "none"); - if (timer == null) { - timer = buildTimer(this.listenerId, "success", queue, "none"); - } - ((Sample) sample).stop(timer); - } - - void failure(Object sample, String queue, String exception) { - Timer timer = this.timers.get(queue + exception); - if (timer == null) { - timer = buildTimer(this.listenerId, "failure", queue, exception); - } - ((Sample) sample).stop(timer); - } - - private Timer buildTimer(String aListenerId, String result, String queue, String exception) { - - Builder builder = Timer.builder("spring.rabbitmq.listener") - .description("Spring RabbitMQ Listener") - .tag("listener.id", aListenerId) - .tag("queue", queue) - .tag("result", result) - .tag("exception", exception); - if (this.tags != null && !this.tags.isEmpty()) { - this.tags.forEach((key, value) -> builder.tag(key, value)); - } - Timer registeredTimer = builder.register(this.registry); - this.timers.put(queue + exception, registeredTimer); - return registeredTimer; - } - - void destroy() { - this.timers.values().forEach(this.registry::remove); - this.timers.clear(); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java index bcd7dffc4f..bfa62b4234 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ import java.util.Collection; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.MessageListener; @@ -37,7 +39,6 @@ import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.task.TaskExecutor; import org.springframework.expression.BeanResolver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -46,6 +47,7 @@ * @author Stephane Nicoll * @author Gary Russell * @author Artem Bilan + * @author Ngoc Nhan * * @since 1.4 * @@ -54,7 +56,7 @@ */ public abstract class AbstractRabbitListenerEndpoint implements RabbitListenerEndpoint, BeanFactoryAware { - private String id; + private @Nullable String id; private final Collection queues = new ArrayList<>(); @@ -62,64 +64,63 @@ public abstract class AbstractRabbitListenerEndpoint implements RabbitListenerEn private boolean exclusive; - private Integer priority; + private @Nullable Integer priority; - private String concurrency; + private @Nullable String concurrency; - private AmqpAdmin admin; + private @Nullable AmqpAdmin admin; - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - private BeanExpressionResolver resolver; + private @Nullable BeanExpressionResolver resolver; - private BeanExpressionContext expressionContext; + private @Nullable BeanExpressionContext expressionContext; - private BeanResolver beanResolver; + private @Nullable BeanResolver beanResolver; - private String group; + private @Nullable String group; - private Boolean autoStartup; + private @Nullable Boolean autoStartup; - private MessageConverter messageConverter; + private @Nullable MessageConverter messageConverter; - private TaskExecutor taskExecutor; + private @Nullable TaskExecutor taskExecutor; - private boolean batchListener; + private @Nullable Boolean batchListener; - private BatchingStrategy batchingStrategy; + private @Nullable BatchingStrategy batchingStrategy; - private AcknowledgeMode ackMode; + private @Nullable AcknowledgeMode ackMode; - private ReplyPostProcessor replyPostProcessor; + private @Nullable ReplyPostProcessor replyPostProcessor; - private String replyContentType; + private @Nullable String replyContentType; private boolean converterWinsContentType = true; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; - if (beanFactory instanceof ConfigurableListableBeanFactory) { - this.resolver = ((ConfigurableListableBeanFactory) beanFactory).getBeanExpressionResolver(); - this.expressionContext = new BeanExpressionContext((ConfigurableListableBeanFactory) beanFactory, null); + if (beanFactory instanceof ConfigurableListableBeanFactory clbf) { + this.resolver = clbf.getBeanExpressionResolver(); + this.expressionContext = new BeanExpressionContext(clbf, null); } this.beanResolver = new BeanFactoryResolver(beanFactory); } - @Nullable - protected BeanFactory getBeanFactory() { + protected @Nullable BeanFactory getBeanFactory() { return this.beanFactory; } - protected BeanExpressionResolver getResolver() { + protected @Nullable BeanExpressionResolver getResolver() { return this.resolver; } - protected BeanResolver getBeanResolver() { + protected @Nullable BeanResolver getBeanResolver() { return this.beanResolver; } - protected BeanExpressionContext getBeanExpressionContext() { + protected @Nullable BeanExpressionContext getBeanExpressionContext() { return this.expressionContext; } @@ -128,7 +129,7 @@ public void setId(String id) { } @Override - public String getId() { + public @Nullable String getId() { return this.id; } @@ -199,7 +200,7 @@ public void setPriority(Integer priority) { * @return the priority of this endpoint or {@code null} if * no priority is set. */ - public Integer getPriority() { + public @Nullable Integer getPriority() { return this.priority; } @@ -209,7 +210,7 @@ public Integer getPriority() { * @param concurrency the concurrency. * @since 2.0 */ - public void setConcurrency(String concurrency) { + public void setConcurrency(@Nullable String concurrency) { this.concurrency = concurrency; } @@ -220,7 +221,7 @@ public void setConcurrency(String concurrency) { * @since 2.0 */ @Override - public String getConcurrency() { + public @Nullable String getConcurrency() { return this.concurrency; } @@ -236,12 +237,12 @@ public void setAdmin(AmqpAdmin admin) { * @return the {@link AmqpAdmin} instance to use or {@code null} if * none is configured. */ - public AmqpAdmin getAdmin() { + public @Nullable AmqpAdmin getAdmin() { return this.admin; } @Override - public String getGroup() { + public @Nullable String getGroup() { return this.group; } @@ -254,7 +255,6 @@ public void setGroup(String group) { this.group = group; } - /** * Override the default autoStartup property. * @param autoStartup the autoStartup. @@ -265,12 +265,12 @@ public void setAutoStartup(Boolean autoStartup) { } @Override - public Boolean getAutoStartup() { + public @Nullable Boolean getAutoStartup() { return this.autoStartup; } @Override - public MessageConverter getMessageConverter() { + public @Nullable MessageConverter getMessageConverter() { return this.messageConverter; } @@ -280,7 +280,7 @@ public void setMessageConverter(MessageConverter messageConverter) { } @Override - public TaskExecutor getTaskExecutor() { + public @Nullable TaskExecutor getTaskExecutor() { return this.taskExecutor; } @@ -293,7 +293,21 @@ public void setTaskExecutor(TaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } + /** + * True if this endpoint is for a batch listener. + * @return true if batch. + */ public boolean isBatchListener() { + return this.batchListener != null && this.batchListener; + } + + /** + * True if this endpoint is for a batch listener. + * @return {@link Boolean#TRUE} if batch. + * @since 3.0 + */ + @Override + public @Nullable Boolean getBatchListener() { return this.batchListener; } @@ -308,8 +322,8 @@ public void setBatchListener(boolean batchListener) { this.batchListener = batchListener; } - @Nullable - public BatchingStrategy getBatchingStrategy() { + @Override + public @Nullable BatchingStrategy getBatchingStrategy() { return this.batchingStrategy; } @@ -319,8 +333,7 @@ public void setBatchingStrategy(BatchingStrategy batchingStrategy) { } @Override - @Nullable - public AcknowledgeMode getAckMode() { + public @Nullable AcknowledgeMode getAckMode() { return this.ackMode; } @@ -329,7 +342,7 @@ public void setAckMode(AcknowledgeMode mode) { } @Override - public ReplyPostProcessor getReplyPostProcessor() { + public @Nullable ReplyPostProcessor getReplyPostProcessor() { return this.replyPostProcessor; } @@ -343,7 +356,7 @@ public void setReplyPostProcessor(ReplyPostProcessor replyPostProcessor) { } @Override - public String getReplyContentType() { + public @Nullable String getReplyContentType() { return this.replyContentType; } @@ -374,16 +387,13 @@ public void setConverterWinsContentType(boolean converterWinsContentType) { public void setupListenerContainer(MessageListenerContainer listenerContainer) { Collection qNames = getQueueNames(); boolean queueNamesEmpty = qNames.isEmpty(); - if (listenerContainer instanceof AbstractMessageListenerContainer) { - AbstractMessageListenerContainer container = (AbstractMessageListenerContainer) listenerContainer; - + if (listenerContainer instanceof AbstractMessageListenerContainer container) { boolean queuesEmpty = getQueues().isEmpty(); if (!queuesEmpty && !queueNamesEmpty) { throw new IllegalStateException("Queues or queue names must be provided but not both for " + this); } if (queuesEmpty) { - Collection names = qNames; - container.setQueueNames(names.toArray(new String[0])); + container.setQueueNames(qNames.toArray(new String[0])); } else { Collection instances = getQueues(); @@ -412,9 +422,9 @@ public void setupListenerContainer(MessageListenerContainer listenerContainer) { * Create a {@link MessageListener} that is able to serve this endpoint for the * specified container. * @param container the {@link MessageListenerContainer} to create a {@link MessageListener}. - * @return a a {@link MessageListener} instance. + * @return a {@link MessageListener} instance. */ - protected abstract MessageListener createMessageListener(MessageListenerContainer container); + protected abstract @Nullable MessageListener createMessageListener(MessageListenerContainer container); private void setupMessageListener(MessageListenerContainer container) { MessageListener messageListener = createMessageListener(container); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 804c1a8887..0400062417 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,12 @@ 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.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.OptionalLong; import java.util.Set; import java.util.concurrent.BlockingQueue; @@ -36,10 +35,19 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.utility.Utility; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpException; @@ -63,18 +71,12 @@ import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.support.ConsumerTagStrategy; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.backoff.BackOffExecution; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.utility.Utility; - /** * Specialized consumer encapsulating knowledge of the broker * connections and having its own lifecycle (start and stop). @@ -87,6 +89,7 @@ * @author Alex Panchenko * @author Johno Crawford * @author Ian Roberts + * @author Cao Weibo */ public class BlockingQueueConsumer { @@ -94,12 +97,14 @@ public class BlockingQueueConsumer { private static final int DEFAULT_RETRY_DECLARATION_INTERVAL = 60000; - private static Log logger = LogFactory.getLog(BlockingQueueConsumer.class); + private static final Log logger = LogFactory.getLog(BlockingQueueConsumer.class); + + private final Lock lifecycleLock = new ReentrantLock(); private final BlockingQueue queue; // When this is non-null the connection has been closed (should never happen in normal operation). - private volatile ShutdownSignalException shutdown; + private volatile @Nullable ShutdownSignalException shutdown; private final String[] queues; @@ -107,8 +112,10 @@ public class BlockingQueueConsumer { private final boolean transactional; + @SuppressWarnings("NullAway.Init") private Channel channel; + @SuppressWarnings("NullAway.Init") private RabbitResourceHolder resourceHolder; private final ConcurrentMap consumers = new ConcurrentHashMap<>(); @@ -127,17 +134,19 @@ public class BlockingQueueConsumer { private final ActiveObjectCounter activeObjectCounter; - private final Map consumerArgs = new HashMap(); + private final Map consumerArgs = new HashMap<>(); private final boolean noLocal; private final boolean exclusive; - private final Set deliveryTags = new LinkedHashSet(); + private final Set deliveryTags = new LinkedHashSet<>(); private final boolean defaultRequeueRejected; - private final Set missingQueues = Collections.synchronizedSet(new HashSet()); + private final Set missingQueues = ConcurrentHashMap.newKeySet(); + + private final Lock missingQueuesLock = new ReentrantLock(); private long retryDeclarationInterval = DEFAULT_RETRY_DECLARATION_INTERVAL; @@ -148,19 +157,21 @@ public class BlockingQueueConsumer { private long lastRetryDeclaration; - private ConsumerTagStrategy tagStrategy; + private @Nullable ConsumerTagStrategy tagStrategy; + @SuppressWarnings("NullAway.Init") private BackOffExecution backOffExecution; private long shutdownTimeout; private boolean locallyTransacted; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; private long consumeDelay; - private java.util.function.Consumer missingQueuePublisher = str -> { }; + private java.util.function.Consumer missingQueuePublisher = str -> { + }; private boolean globalQos; @@ -168,10 +179,13 @@ public class BlockingQueueConsumer { private volatile boolean normalCancel; + @SuppressWarnings("NullAway.Init") volatile Thread thread; // NOSONAR package protected volatile boolean declaring; // NOSONAR package protected + private @Nullable MessageAckListener messageAckListener; + /** * Create a consumer. The consumer must not attempt to use * the connection factory or communicate with the broker @@ -188,6 +202,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, MessagePropertiesConverter messagePropertiesConverter, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, String... queues) { + this(connectionFactory, messagePropertiesConverter, activeObjectCounter, acknowledgeMode, transactional, prefetchCount, true, queues); } @@ -209,6 +224,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, MessagePropertiesConverter messagePropertiesConverter, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, String... queues) { + this(connectionFactory, messagePropertiesConverter, activeObjectCounter, acknowledgeMode, transactional, prefetchCount, defaultRequeueRejected, null, queues); } @@ -232,6 +248,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, @Nullable Map consumerArgs, String... queues) { + this(connectionFactory, messagePropertiesConverter, activeObjectCounter, acknowledgeMode, transactional, prefetchCount, defaultRequeueRejected, consumerArgs, false, queues); } @@ -256,6 +273,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, @Nullable Map consumerArgs, boolean exclusive, String... queues) { + this(connectionFactory, messagePropertiesConverter, activeObjectCounter, acknowledgeMode, transactional, prefetchCount, defaultRequeueRejected, consumerArgs, false, exclusive, queues); } @@ -282,6 +300,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, @Nullable Map consumerArgs, boolean noLocal, boolean exclusive, String... queues) { + this.connectionFactory = connectionFactory; this.messagePropertiesConverter = messagePropertiesConverter; this.activeObjectCounter = activeObjectCounter; @@ -289,13 +308,13 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, this.transactional = transactional; this.prefetchCount = prefetchCount; this.defaultRequeueRejected = defaultRequeueRejected; - if (consumerArgs != null && consumerArgs.size() > 0) { + if (!CollectionUtils.isEmpty(consumerArgs)) { this.consumerArgs.putAll(consumerArgs); } this.noLocal = noLocal; this.exclusive = exclusive; this.queues = Arrays.copyOf(queues, queues.length); - this.queue = new LinkedBlockingQueue(prefetchCount); + this.queue = new LinkedBlockingQueue<>(queues.length == 0 ? prefetchCount : prefetchCount * queues.length); } public Channel getChannel() { @@ -304,8 +323,8 @@ public Channel getChannel() { public Collection getConsumerTags() { return this.consumers.values().stream() - .map(c -> c.getConsumerTag()) - .filter(tag -> tag != null) + .map(DefaultConsumer::getConsumerTag) + .filter(Objects::nonNull) .collect(Collectors.toList()); } @@ -374,7 +393,7 @@ public void setLocallyTransacted(boolean locallyTransacted) { this.locallyTransacted = locallyTransacted; } - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + public void setApplicationEventPublisher(@Nullable ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } @@ -398,6 +417,17 @@ public void setConsumeDelay(long consumeDelay) { this.consumeDelay = consumeDelay; } + /** + * Set a {@link MessageAckListener} to use when ack a message(messages) in + * {@link AcknowledgeMode#AUTO} mode. + * @param messageAckListener the messageAckListener. + * @since 2.4.6 + */ + public void setMessageAckListener(MessageAckListener messageAckListener) { + Assert.notNull(messageAckListener, "'messageAckListener' cannot be null"); + this.messageAckListener = messageAckListener; + } + /** * Clear the delivery tags when rolling back with an external transaction * manager. @@ -434,16 +464,17 @@ int getQueueCount() { } protected void basicCancel() { - basicCancel(false); + basicCancel(true); } protected void basicCancel(boolean expected) { this.normalCancel = expected; - getConsumerTags().forEach(consumerTag -> { - if (this.channel.isOpen()) { - RabbitUtils.cancel(this.channel, consumerTag); - } - }); + getConsumerTags() + .forEach(consumerTag -> { + if (this.channel.isOpen()) { + RabbitUtils.cancel(this.channel, consumerTag); + } + }); this.cancelled.set(true); this.abortStarted = System.currentTimeMillis(); } @@ -462,8 +493,9 @@ protected boolean cancelled() { * Check if we are in shutdown mode and if so throw an exception. */ private void checkShutdown() { - if (this.shutdown != null) { - throw Utility.fixStackTrace(this.shutdown); + ShutdownSignalException shutdownToUse = this.shutdown; + if (shutdownToUse != null) { + throw Utility.fixStackTrace(shutdownToUse); } } @@ -473,12 +505,11 @@ private void checkShutdown() { * shutdown. If delivery is null, we may be in shutdown mode. Check and see. * @param delivery the delivered message contents. * @return A message built from the contents. - * @throws InterruptedException if the thread is interrupted. */ - @Nullable - private Message handle(@Nullable Delivery delivery) { - if ((delivery == null && this.shutdown != null)) { - throw this.shutdown; + private @Nullable Message handle(@Nullable Delivery delivery) { + ShutdownSignalException shutdownToUse = this.shutdown; + if (delivery == null && shutdownToUse != null) { + throw shutdownToUse; } if (delivery == null) { return null; @@ -510,10 +541,7 @@ private Message handle(@Nullable Delivery delivery) { */ @Nullable public Message nextMessage() throws InterruptedException, ShutdownSignalException { - if (logger.isTraceEnabled()) { - logger.trace("Retrieving delivery for " + this); - } - return handle(this.queue.take()); + return nextMessage(-1); } /** @@ -529,11 +557,12 @@ public Message nextMessage(long timeout) throws InterruptedException, ShutdownSi logger.trace("Retrieving delivery for " + this); } checkShutdown(); - if (this.missingQueues.size() > 0) { + if (!this.missingQueues.isEmpty()) { checkMissingQueues(); } - Message message = handle(this.queue.poll(timeout, TimeUnit.MILLISECONDS)); + Message message = handle(timeout < 0 ? this.queue.take() : this.queue.poll(timeout, TimeUnit.MILLISECONDS)); if (message == null && this.cancelled.get()) { + this.activeObjectCounter.release(this); throw new ConsumerCancelledException(); } return message; @@ -546,7 +575,8 @@ public Message nextMessage(long timeout) throws InterruptedException, ShutdownSi private void checkMissingQueues() { long now = System.currentTimeMillis(); if (now - this.retryDeclarationInterval > this.lastRetryDeclaration) { - synchronized (this.missingQueues) { + this.missingQueuesLock.lock(); + try { Iterator iterator = this.missingQueues.iterator(); while (iterator.hasNext()) { boolean available = true; @@ -554,7 +584,8 @@ private void checkMissingQueues() { Connection connection = null; // NOSONAR - RabbitUtils Channel channelForCheck = null; try { - channelForCheck = this.connectionFactory.createConnection().createChannel(false); + connection = this.connectionFactory.createConnection(); + channelForCheck = connection.createChannel(false); channelForCheck.queueDeclarePassive(queueToCheck); if (logger.isInfoEnabled()) { logger.info("Queue '" + queueToCheck + "' is now available"); @@ -581,10 +612,14 @@ private void checkMissingQueues() { } } } + finally { + this.missingQueuesLock.unlock(); + } this.lastRetryDeclaration = now; } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void start() throws AmqpException { if (logger.isDebugEnabled()) { logger.debug("Starting consumer " + this); @@ -596,7 +631,7 @@ public void start() throws AmqpException { this.resourceHolder = ConnectionFactoryUtils.getTransactionalResourceHolder(this.connectionFactory, this.transactional); this.channel = this.resourceHolder.getChannel(); - ClosingRecoveryListener.addRecoveryListenerIfNecessary(this.channel); // NOSONAR never null here + ClosingRecoveryListener.addRecoveryListenerIfNecessary(this.channel); } catch (AmqpAuthenticationException e) { throw new FatalListenerStartupException("Authentication failure", e); @@ -722,11 +757,12 @@ private void attemptPassiveDeclarations() { } catch (IllegalArgumentException e) { try { - if (this.channel instanceof ChannelProxy) { - ((ChannelProxy) this.channel).getTargetChannel().close(); + if (this.channel instanceof ChannelProxy proxy) { + proxy.getTargetChannel().close(); } } catch (TimeoutException e1) { + // Ignore } throw new FatalListenerStartupException("Illegal Argument on Queue Declaration", e); } @@ -750,23 +786,41 @@ private void attemptPassiveDeclarations() { } } - public synchronized void stop() { - if (this.abortStarted == 0) { // signal handle delivery to use offer - this.abortStarted = System.currentTimeMillis(); - } - if (!this.cancelled()) { + public void stop() { + this.lifecycleLock.lock(); + try { + if (this.abortStarted == 0) { // signal handle delivery to use offer + this.abortStarted = System.currentTimeMillis(); + } + if (!cancelled()) { + basicCancel(true); + } try { - RabbitUtils.closeMessageConsumer(this.channel, getConsumerTags(), this.transactional); + if (this.transactional) { + /* + * Re-queue in-flight messages if any + * (after the consumer is cancelled to prevent the broker from simply sending them back to us). + * Does not require a tx.commit. + */ + this.channel.basicRecover(true); + } } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug("Error closing consumer " + this, e); } } + if (logger.isDebugEnabled()) { + logger.debug("Closing Rabbit Channel: " + this.channel); + } + forceCloseAndClearQueue(); } - if (logger.isDebugEnabled()) { - logger.debug("Closing Rabbit Channel: " + this.channel); + finally { + this.lifecycleLock.unlock(); } + } + + public void forceCloseAndClearQueue() { RabbitUtils.setPhysicalCloseRequired(this.channel, true); ConnectionFactoryUtils.releaseResources(this.resourceHolder); this.deliveryTags.clear(); @@ -779,6 +833,17 @@ public synchronized void stop() { * @param ex the thrown application exception or error */ public void rollbackOnExceptionIfNecessary(Throwable ex) { + rollbackOnExceptionIfNecessary(ex, -1); + } + + /** + * Perform a rollback, handling rollback exceptions properly. + * @param ex the thrown application exception or error + * @param tag delivery tag; when specified (greater than or equal to 0) only that + * message is nacked. + * @since 2.2.21. + */ + public void rollbackOnExceptionIfNecessary(Throwable ex, long tag) { boolean ackRequired = !this.acknowledgeMode.isAutoAck() && (!this.acknowledgeMode.isManual() || ContainerUtils.isRejectManual(ex)); @@ -790,14 +855,20 @@ public void rollbackOnExceptionIfNecessary(Throwable ex) { RabbitUtils.rollbackIfNecessary(this.channel); } if (ackRequired) { - OptionalLong deliveryTag = this.deliveryTags.stream().mapToLong(l -> l).max(); - if (deliveryTag.isPresent()) { - this.channel.basicNack(deliveryTag.getAsLong(), true, - ContainerUtils.shouldRequeue(this.defaultRequeueRejected, ex, logger)); + if (tag < 0) { + OptionalLong deliveryTag = this.deliveryTags.stream().mapToLong(l -> l).max(); + if (deliveryTag.isPresent()) { + this.channel.basicNack(deliveryTag.getAsLong(), true, + ContainerUtils.shouldRequeue(this.defaultRequeueRejected, ex, logger)); + } + if (this.transactional) { + // Need to commit the reject (=nack) + RabbitUtils.commitIfNecessary(this.channel); + } } - if (this.transactional) { - // Need to commit the reject (=nack) - RabbitUtils.commitIfNecessary(this.channel); + else { + this.channel.basicNack(tag, false, + ContainerUtils.shouldRequeue(this.defaultRequeueRejected, ex, logger)); } } } @@ -806,18 +877,24 @@ public void rollbackOnExceptionIfNecessary(Throwable ex) { throw RabbitExceptionTranslator.convertRabbitAccessException(e); // NOSONAR stack trace loss } finally { - this.deliveryTags.clear(); + if (tag < 0) { + this.deliveryTags.clear(); + } + else { + this.deliveryTags.remove(tag); + } } } /** * Perform a commit or message acknowledgement, as appropriate. + * NOTE: This method was never been intended tobe public. * @param localTx Whether the channel is locally transacted. + * @param forceAck perform {@link Channel#basicAck(long, boolean)} independently of {@link #acknowledgeMode}. * @return true if at least one delivery tag exists. - * @throws IOException Any IOException. + * @since 3.1.2 */ - public boolean commitIfNecessary(boolean localTx) throws IOException { - + boolean commitIfNecessary(boolean localTx, boolean forceAck) { if (this.deliveryTags.isEmpty()) { return false; } @@ -825,30 +902,55 @@ public boolean commitIfNecessary(boolean localTx) throws IOException { /* * If we have a TX Manager, but no TX, act like we are locally transacted. */ - boolean isLocallyTransacted = localTx - || (this.transactional - && TransactionSynchronizationManager.getResource(this.connectionFactory) == null); + boolean isLocallyTransacted = + localTx || + (this.transactional && + TransactionSynchronizationManager.getResource(this.connectionFactory) == null); try { + boolean ackRequired = forceAck || (!this.acknowledgeMode.isAutoAck() && !this.acknowledgeMode.isManual()); - boolean ackRequired = !this.acknowledgeMode.isAutoAck() && !this.acknowledgeMode.isManual(); - - if (ackRequired && (!this.transactional || isLocallyTransacted)) { - long deliveryTag = new ArrayList(this.deliveryTags).get(this.deliveryTags.size() - 1); - this.channel.basicAck(deliveryTag, true); + if (ackRequired && (!this.transactional || (isLocallyTransacted && !cancelled()))) { + OptionalLong deliveryTag = this.deliveryTags.stream().mapToLong(l -> l).max(); + deliveryTag.ifPresent((tag) -> { + try { + this.channel.basicAck(tag, true); + notifyMessageAckListener(true, tag, null); + } + catch (Exception e) { + logger.error("Error acking.", e); + notifyMessageAckListener(false, tag, e); + } + }); } if (isLocallyTransacted) { // For manual acks we still need to commit RabbitUtils.commitIfNecessary(this.channel); } - } finally { this.deliveryTags.clear(); } return true; + } + /** + * Notify MessageAckListener set on message listener. + * @param success Whether ack succeeded. + * @param deliveryTag The deliveryTag of ack. + * @param cause If an exception occurs. + * @since 2.4.6 + */ + private void notifyMessageAckListener(boolean success, long deliveryTag, @Nullable Throwable cause) { + if (this.messageAckListener != null) { + try { + this.messageAckListener.onComplete(success, deliveryTag, cause); + } + catch (Exception e) { + logger.error("An exception occurred in MessageAckListener.", e); + } + } } @Override @@ -927,6 +1029,7 @@ public void handleCancelOk(String consumerTag) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) { + if (logger.isDebugEnabled()) { logger.debug("Storing delivery for consumerTag: '" + consumerTag + "' with deliveryTag: '" + envelope.getDeliveryTag() + "' in " @@ -986,7 +1089,7 @@ private DeclarationException(Throwable t) { super("Failed to declare queue(s):", t); } - private final List failedQueues = new ArrayList(); + private final List failedQueues = new ArrayList<>(); private void addFailedQueue(String queue) { this.failedQueues.add(queue); @@ -998,7 +1101,7 @@ private List getFailedQueues() { @Override public String getMessage() { - return super.getMessage() + this.failedQueues.toString(); + return super.getMessage() + this.failedQueues; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java index d58d9918ed..488a91edc9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,13 @@ package org.springframework.amqp.rabbit.listener; +import java.lang.reflect.UndeclaredThrowableException; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.ImmediateAcknowledgeAmqpException; @@ -49,6 +51,7 @@ * {@link AmqpRejectAndDontRequeueException}. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.3.2 * */ @@ -88,7 +91,7 @@ protected boolean isDiscardFatalsWithXDeath() { } /** - * Set to false to disable the (now) default behavior of logging and discarding + * Set to {@code false} to disable the (now) default behavior of logging and discarding * messages that cause fatal exceptions and have an `x-death` header; which * usually means that the message has been republished after previously being * sent to a DLQ. @@ -110,7 +113,7 @@ protected boolean isRejectManual() { } /** - * Set to false to NOT reject a fatal message when MANUAL ack mode is being used. + * Set to {@code false} to NOT reject a fatal message when MANUAL ack mode is being used. * @param rejectManual false to leave the message in an unack'd state. * @since 2.1.9 */ @@ -131,11 +134,11 @@ protected FatalExceptionStrategy getExceptionStrategy() { public void handleError(Throwable t) { log(t); if (!this.causeChainContainsARADRE(t) && this.exceptionStrategy.isFatal(t)) { - if (this.discardFatalsWithXDeath && t instanceof ListenerExecutionFailedException) { - Message failed = ((ListenerExecutionFailedException) t).getFailedMessage(); + if (this.discardFatalsWithXDeath && t instanceof ListenerExecutionFailedException lefe) { + Message failed = lefe.getFailedMessage(); if (failed != null) { List> xDeath = failed.getMessageProperties().getXDeathHeader(); - if (xDeath != null && xDeath.size() > 0) { + if (xDeath != null && !xDeath.isEmpty()) { this.logger.error("x-death header detected on a message with a fatal exception; " + "perhaps requeued from a DLQ? - discarding: " + failed); handleDiscarded(failed); @@ -200,19 +203,19 @@ public static class DefaultExceptionStrategy implements FatalExceptionStrategy { @Override public boolean isFatal(Throwable t) { Throwable cause = t.getCause(); - while (cause instanceof MessagingException - && !(cause instanceof org.springframework.messaging.converter.MessageConversionException) - && !(cause instanceof MethodArgumentResolutionException)) { + while ((cause instanceof MessagingException || cause instanceof UndeclaredThrowableException) + && !isCauseFatal(cause)) { + cause = cause.getCause(); } - if (t instanceof ListenerExecutionFailedException && isCauseFatal(cause)) { - logFatalException((ListenerExecutionFailedException) t, cause); + if (t instanceof ListenerExecutionFailedException lefe && isCauseFatal(cause)) { + logFatalException(lefe, cause); return true; } return false; } - private boolean isCauseFatal(Throwable cause) { + private boolean isCauseFatal(@Nullable Throwable cause) { return cause instanceof MessageConversionException // NOSONAR boolean complexity || cause instanceof org.springframework.messaging.converter.MessageConversionException || cause instanceof MethodArgumentResolutionException @@ -228,7 +231,7 @@ private boolean isCauseFatal(Throwable cause) { * @param cause the root cause (skipping any general {@link MessagingException}s). * @since 2.2.4 */ - protected void logFatalException(ListenerExecutionFailedException t, Throwable cause) { + protected void logFatalException(ListenerExecutionFailedException t, @Nullable Throwable cause) { if (this.logger.isWarnEnabled()) { this.logger.warn( "Fatal message conversion error; message rejected; " @@ -242,7 +245,7 @@ protected void logFatalException(ListenerExecutionFailedException t, Throwable c * @param cause the cause * @return true if the cause is fatal. */ - protected boolean isUserCauseFatal(Throwable cause) { + protected boolean isUserCauseFatal(@Nullable Throwable cause) { return false; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 3a816caa44..31a210bfba 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,12 @@ import java.io.IOException; import java.lang.reflect.Constructor; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -35,16 +36,25 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownSignalException; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpException; import org.springframework.amqp.AmqpIOException; +import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.Message; @@ -57,11 +67,12 @@ import org.springframework.amqp.rabbit.connection.ConsumerChannelRegistry; import org.springframework.amqp.rabbit.connection.RabbitResourceHolder; import org.springframework.amqp.rabbit.connection.RabbitUtils; +import org.springframework.amqp.rabbit.connection.RoutingConnectionFactory; import org.springframework.amqp.rabbit.connection.SimpleResourceHolder; import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbit.support.ActiveObjectCounter; import org.springframework.amqp.rabbit.transaction.RabbitTransactionManager; -import org.springframework.lang.Nullable; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.transaction.PlatformTransactionManager; @@ -77,12 +88,6 @@ import org.springframework.util.StringUtils; import org.springframework.util.backoff.BackOffExecution; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.ShutdownSignalException; - /** * The {@code SimpleMessageListenerContainer} is not so simple. Recent changes to the * rabbitmq java client has facilitated a much simpler listener container that invokes the @@ -92,6 +97,7 @@ * @author Gary Russell * @author Artem Bilan * @author Nicolas Ristock + * @author Cao Weibo * * @since 2.0 * @@ -110,11 +116,11 @@ public class DirectMessageListenerContainer extends AbstractMessageListenerConta private final Set removedQueues = ConcurrentHashMap.newKeySet(); - private final MultiValueMap consumersByQueue = new LinkedMultiValueMap<>(); + private final MultiValueMap consumersByQueue = new LinkedMultiValueMap<>(); private final ActiveObjectCounter cancellationLock = new ActiveObjectCounter<>(); - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; private boolean taskSchedulerSet; @@ -134,7 +140,7 @@ public class DirectMessageListenerContainer extends AbstractMessageListenerConta private volatile int consumersPerQueue = 1; - private volatile ScheduledFuture consumerMonitorTask; + private volatile @Nullable ScheduledFuture consumerMonitorTask; private volatile long lastAlertAt; @@ -144,6 +150,7 @@ public class DirectMessageListenerContainer extends AbstractMessageListenerConta * Create an instance; {@link #setConnectionFactory(ConnectionFactory)} must * be called before starting. */ + @SuppressWarnings("this-escape") public DirectMessageListenerContainer() { setMissingQueuesFatal(false); doSetPossibleAuthenticationFailureFatal(false); @@ -153,6 +160,7 @@ public DirectMessageListenerContainer() { * Create an instance with the provided connection factory. * @param connectionFactory the connection factory. */ + @SuppressWarnings("this-escape") public DirectMessageListenerContainer(ConnectionFactory connectionFactory) { setConnectionFactory(connectionFactory); setMissingQueuesFatal(false); @@ -197,7 +205,7 @@ public void setTaskScheduler(TaskScheduler taskScheduler) { /** * Set how often to run a task to check for failed consumers and idle containers. - * @param monitorInterval the interval; default 10000 but it will be adjusted down + * @param monitorInterval the interval; default 10000, but it will be adjusted down * to the smallest of this, {@link #setIdleEventInterval(long) idleEventInterval} / 2 * (if configured) or * {@link #setFailedDeclarationRetryInterval(long) failedDeclarationRetryInterval}. @@ -265,8 +273,8 @@ public void addQueues(Queue... queues) { Assert.noNullElements(queues, "'queues' cannot contain null elements"); try { Arrays.stream(queues) - .map(q -> q.getActualName()) - .forEach(this.removedQueues::remove); + .map(Queue::getActualName) + .forEach(this.removedQueues::remove); addQueues(Arrays.stream(queues).map(Queue::getName)); } catch (AmqpIOException e) { @@ -277,7 +285,8 @@ public void addQueues(Queue... queues) { private void addQueues(Stream queueNameStream) { if (isRunning()) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { checkStartState(); Set current = getQueueNamesAsSet(); queueNameStream.forEach(queue -> { @@ -290,6 +299,9 @@ private void addQueues(Stream queueNameStream) { } }); } + finally { + this.consumersLock.unlock(); + } } } @@ -307,7 +319,8 @@ public boolean removeQueues(Queue... queues) { private void removeQueues(Stream queueNames) { if (isRunning()) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { checkStartState(); queueNames.map(queue -> { this.removedQueues.add(queue); @@ -317,24 +330,31 @@ private void removeQueues(Stream queueNames) { .flatMap(Collection::stream) .forEach(this::cancelConsumer); } + finally { + this.consumersLock.unlock(); + } } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void adjustConsumers(int newCount) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { checkStartState(); this.consumersToRestart.clear(); for (String queue : getQueueNames()) { - while (this.consumersByQueue.get(queue) == null - || this.consumersByQueue.get(queue).size() < newCount) { // NOSONAR never null - List cBQ = this.consumersByQueue.get(queue); + while (isActive() && + (this.consumersByQueue.get(queue) == null + || this.consumersByQueue.get(queue).size() < newCount)) { // NOSONAR never null + List<@Nullable SimpleConsumer> cBQ = this.consumersByQueue.get(queue); int index = 0; if (cBQ != null) { // find a gap or set the index to the end List indices = cBQ.stream() - .map(cons -> cons.getIndex()) + .filter(Objects::nonNull) + .map(SimpleConsumer::getIndex) .sorted() - .collect(Collectors.toList()); + .toList(); for (index = 0; index < indices.size(); index++) { if (index < indices.get(index)) { break; @@ -346,10 +366,13 @@ private void adjustConsumers(int newCount) { reduceConsumersIfIdle(newCount, queue); } } + finally { + this.consumersLock.unlock(); + } } private void reduceConsumersIfIdle(int newCount, String queue) { - List consumerList = this.consumersByQueue.get(queue); + List<@Nullable SimpleConsumer> consumerList = this.consumersByQueue.get(queue); if (consumerList != null && consumerList.size() > newCount) { int delta = consumerList.size() - newCount; for (int i = 0; i < delta; i++) { @@ -365,9 +388,8 @@ private void reduceConsumersIfIdle(int newCount, String queue) { } /** - * When adjusting down, return a consumer that can be canceled. Called while - * synchronized on consumersMonitor. - * @return the consumer index or -1 if non idle. + * When adjusting down, return a consumer that can be canceled. Called while locked on {@link #consumersLock}. + * @return the consumer index or -1 if non-idle. * @since 2.0.6 */ protected int findIdleConsumer() { @@ -391,7 +413,7 @@ private void checkStartState() { protected void doInitialize() { if (this.taskScheduler == null) { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); - threadPoolTaskScheduler.setThreadNamePrefix(getBeanName() + "-consumerMonitor-"); + threadPoolTaskScheduler.setThreadNamePrefix(getListenerId() + "-consumerMonitor-"); threadPoolTaskScheduler.afterPropertiesSet(); this.taskScheduler = threadPoolTaskScheduler; } @@ -454,11 +476,11 @@ protected void actualStart() { } } + @SuppressWarnings("try") protected void checkConnect() { if (isPossibleAuthenticationFailureFatal()) { - Connection connection = null; - try { - getConnectionFactory().createConnection(); + try (Connection __ = getConnectionFactory().createConnection()) { + // Authentication attempt } catch (AmqpAuthenticationException ex) { this.logger.debug("Failed to authenticate", ex); @@ -466,25 +488,22 @@ protected void checkConnect() { } catch (Exception ex) { // NOSONAR } - finally { - if (connection != null) { - connection.close(); - } - } } } private void startMonitor(long idleEventInterval, final Map namesToQueues) { + Assert.state(this.taskScheduler != null, "taskScheduler must be provided"); this.consumerMonitorTask = this.taskScheduler.scheduleAtFixedRate(() -> { long now = System.currentTimeMillis(); checkIdle(idleEventInterval, now); checkConsumers(now); if (this.lastRestartAttempt + getFailedDeclarationRetryInterval() < now) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.started) { List restartableConsumers = new ArrayList<>(this.consumersToRestart); this.consumersToRestart.clear(); - if (restartableConsumers.size() > 0) { + if (!restartableConsumers.isEmpty()) { doRedeclareElementsIfNecessary(); } Iterator iterator = restartableConsumers.iterator(); @@ -507,9 +526,12 @@ private void startMonitor(long idleEventInterval, final Map names this.lastRestartAttempt = now; } } + finally { + this.consumersLock.unlock(); + } } processMonitorTask(); - }, this.monitorInterval); + }, Duration.ofMillis(this.monitorInterval)); } private void checkIdle(long idleEventInterval, long now) { @@ -522,7 +544,8 @@ private void checkIdle(long idleEventInterval, long now) { private void checkConsumers(long now) { final List consumersToCancel; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { consumersToCancel = this.consumers.stream() .filter(consumer -> { boolean open = consumer.getChannel().isOpen() && !consumer.isAckFailed() @@ -531,13 +554,16 @@ private void checkConsumers(long now) { try { consumer.ackIfNecessary(now); } - catch (IOException e) { + catch (Exception e) { this.logger.error("Exception while sending delayed ack", e); } } return !open; }) - .collect(Collectors.toList()); + .toList(); + } + finally { + this.consumersLock.unlock(); } consumersToCancel .forEach(consumer -> { @@ -577,7 +603,8 @@ private boolean restartConsumer(final Map namesToQueues, List namesToQueues, List list = this.consumersByQueue.get(queue); + List<@Nullable SimpleConsumer> list = this.consumersByQueue.get(queue); // Possible race with setConsumersPerQueue and the task launched by start() if (CollectionUtils.isEmpty(list)) { for (int i = 0; i < this.consumersPerQueue; i++) { @@ -709,8 +744,11 @@ private void doConsumeFromQueue(String queue, int index) { return; } String routingLookupKey = getRoutingLookupKey(); + RoutingConnectionFactory routingConnectionFactory = null; if (routingLookupKey != null) { - SimpleResourceHolder.push(getRoutingConnectionFactory(), routingLookupKey); // NOSONAR both never null here + routingConnectionFactory = getRoutingConnectionFactory(); + Assert.state(routingConnectionFactory != null, "The 'routingConnectionFactory' must be provided"); + SimpleResourceHolder.push(routingConnectionFactory, routingLookupKey); } Connection connection = null; // NOSONAR (close) try { @@ -724,12 +762,13 @@ private void doConsumeFromQueue(String queue, int index) { : new AmqpConnectException(e); } finally { - if (routingLookupKey != null) { - SimpleResourceHolder.pop(getRoutingConnectionFactory()); // NOSONAR never null here + if (routingConnectionFactory != null) { + SimpleResourceHolder.pop(routingConnectionFactory); } } SimpleConsumer consumer = consume(queue, index, connection); - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (consumer != null) { this.cancellationLock.add(consumer); this.consumers.add(consumer); @@ -737,15 +776,18 @@ private void doConsumeFromQueue(String queue, int index) { if (this.logger.isInfoEnabled()) { this.logger.info(consumer + " started"); } - if (getApplicationEventPublisher() != null) { - getApplicationEventPublisher().publishEvent(new AsyncConsumerStartedEvent(this, consumer)); + ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new AsyncConsumerStartedEvent(this, consumer)); } } } + finally { + this.consumersLock.unlock(); + } } - @Nullable - private SimpleConsumer consume(String queue, int index, Connection connection) { + private @Nullable SimpleConsumer consume(String queue, int index, Connection connection) { Channel channel = null; SimpleConsumer consumer = null; try { @@ -769,6 +811,9 @@ private SimpleConsumer consume(String queue, int index, Connection connection) { catch (AmqpApplicationContextClosedException e) { throw new AmqpConnectException(e); } + catch (AmqpTimeoutException timeoutException) { + throw timeoutException; + } catch (Exception e) { RabbitUtils.closeChannel(channel); RabbitUtils.closeConnection(connection); @@ -778,26 +823,23 @@ private SimpleConsumer consume(String queue, int index, Connection connection) { return consumer; } - @Nullable - private SimpleConsumer handleConsumeException(String queue, int index, @Nullable SimpleConsumer consumerArg, - Exception e) { + private @Nullable SimpleConsumer handleConsumeException(String queue, int index, + @Nullable SimpleConsumer consumerArg, Exception ex) { SimpleConsumer consumer = consumerArg; - if (e.getCause() instanceof ShutdownSignalException - && e.getCause().getMessage().contains("in exclusive use")) { - getExclusiveConsumerExceptionLogger().log(logger, - "Exclusive consumer failure", e.getCause()); - publishConsumerFailedEvent("Consumer raised exception, attempting restart", false, e); - } - else if (e.getCause() instanceof ShutdownSignalException - && RabbitUtils.isPassiveDeclarationChannelClose((ShutdownSignalException) e.getCause())) { + if (RabbitUtils.exclusiveAccesssRefused(ex)) { + getExclusiveConsumerExceptionLogger().log(logger, "Exclusive consumer failure", ex.getCause()); + publishConsumerFailedEvent("Consumer raised exception, attempting restart", false, ex); + } + else if (ex.getCause() instanceof ShutdownSignalException + && RabbitUtils.isPassiveDeclarationChannelClose((ShutdownSignalException) ex.getCause())) { publishMissingQueueEvent(queue); this.logger.error("Queue not present, scheduling consumer " - + (consumer == null ? "for queue " + queue : consumer) + " for restart", e); + + (consumer == null ? "for queue " + queue : consumer) + " for restart", ex); } else if (this.logger.isWarnEnabled()) { this.logger.warn("basicConsume failed, scheduling consumer " - + (consumer == null ? "for queue " + queue : consumer) + " for restart", e); + + (consumer == null ? "for queue " + queue : consumer) + " for restart", ex); } if (consumer == null) { @@ -811,10 +853,12 @@ else if (this.logger.isWarnEnabled()) { } @Override - protected void doShutdown() { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { LinkedList canceledConsumers = null; boolean waitForConsumers = false; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.started || this.aborted) { // Copy in the same order to avoid ConcurrentModificationException during remove in the // cancelConsumer(). @@ -823,50 +867,75 @@ protected void doShutdown() { waitForConsumers = true; } } + finally { + this.consumersLock.unlock(); + } if (waitForConsumers) { - try { - if (this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS)) { - this.logger.info("Successfully waited for consumers to finish."); - } - else { - this.logger.info("Consumers not finished."); - if (isForceCloseChannel()) { - canceledConsumers.forEach(consumer -> { - String eventMessage = "Closing channel for unresponsive consumer: " + consumer; - if (logger.isWarnEnabled()) { - logger.warn(eventMessage); - } - consumer.cancelConsumer(eventMessage); - }); + LinkedList consumersToWait = canceledConsumers; + Runnable awaitShutdown = () -> { + try { + if (this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS)) { + this.logger.info("Successfully waited for consumers to finish."); + } + else { + this.logger.info("Consumers not finished."); + if (isForceCloseChannel() || this.stopNow.get()) { + consumersToWait.forEach(consumer -> { + String eventMessage = "Closing channel for unresponsive consumer: " + consumer; + if (logger.isWarnEnabled()) { + logger.warn(eventMessage); + } + consumer.cancelConsumer(eventMessage); + }); + } } } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + this.logger.warn("Interrupted waiting for consumers. Continuing with shutdown."); + } + finally { + this.startedLatch = new CountDownLatch(1); + this.started = false; + this.aborted = false; + this.hasStopped = true; + } + this.stopNow.set(false); + runCallbackIfNotNull(callback); + }; + if (callback == null) { + awaitShutdown.run(); } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - this.logger.warn("Interrupted waiting for consumers. Continuing with shutdown."); - } - finally { - this.startedLatch = new CountDownLatch(1); - this.started = false; - this.aborted = false; - this.hasStopped = true; + else { + getTaskExecutor().execute(awaitShutdown); } } } + private void runCallbackIfNotNull(@Nullable Runnable callback) { + if (callback != null) { + callback.run(); + } + } + /** - * Must hold this.consumersMonitor. + * Must hold this.consumersLock. * @param consumers a copy of this.consumers. */ private void actualShutDown(List consumers) { - Assert.state(getTaskExecutor() != null, "Cannot shut down if not initialized"); this.logger.debug("Shutting down"); - consumers.forEach(this::cancelConsumer); + if (isForceStop()) { + this.stopNow.set(true); + } + else { + consumers.forEach(this::cancelConsumer); + } this.consumers.clear(); this.consumersByQueue.clear(); this.logger.debug("All consumers canceled"); - if (this.consumerMonitorTask != null) { - this.consumerMonitorTask.cancel(true); + ScheduledFuture consumerMonitorTaskToUse = this.consumerMonitorTask; + if (consumerMonitorTaskToUse != null) { + consumerMonitorTaskToUse.cancel(true); this.consumerMonitorTask = null; } } @@ -876,17 +945,21 @@ private void cancelConsumer(SimpleConsumer consumer) { if (this.logger.isDebugEnabled()) { this.logger.debug("Canceling " + consumer); } - synchronized (consumer) { + consumer.lock.lock(); + try { consumer.setCanceled(true); if (this.messagesPerAck > 1) { try { consumer.ackIfNecessary(0L); } - catch (IOException e) { + catch (Exception e) { this.logger.error("Exception while sending delayed ack", e); } } } + finally { + consumer.lock.unlock(); + } RabbitUtils.cancel(consumer.getChannel(), consumer.getConsumerTag()); } finally { @@ -913,11 +986,11 @@ protected void consumerRemoved(SimpleConsumer consumer) { /** * The consumer object. */ - final class SimpleConsumer extends DefaultConsumer { + protected final class SimpleConsumer extends DefaultConsumer { private final Log logger = DirectMessageListenerContainer.this.logger; - private final Connection connection; + private final @Nullable Connection connection; private final String queue; @@ -927,7 +1000,7 @@ final class SimpleConsumer extends DefaultConsumer { private final ConnectionFactory connectionFactory = getConnectionFactory(); - private final PlatformTransactionManager transactionManager = getTransactionManager(); + private final @Nullable PlatformTransactionManager transactionManager = getTransactionManager(); private final TransactionAttribute transactionAttribute = getTransactionAttribute(); @@ -937,7 +1010,11 @@ final class SimpleConsumer extends DefaultConsumer { private final long ackTimeout = DirectMessageListenerContainer.this.ackTimeout; - private final Channel targetChannel; + private final @Nullable Channel targetChannel; + + private final Lock lock = new ReentrantLock(); + + private final AtomicInteger epoch = new AtomicInteger(0); private int pendingAcks; @@ -945,11 +1022,10 @@ final class SimpleConsumer extends DefaultConsumer { private long latestDeferredDeliveryTag; + @SuppressWarnings("NullAway.Init") private volatile String consumerTag; - private volatile int epoch; - - private volatile TransactionTemplate transactionTemplate; + private volatile @Nullable TransactionTemplate transactionTemplate; private volatile boolean canceled; @@ -961,8 +1037,8 @@ final class SimpleConsumer extends DefaultConsumer { this.queue = queue; this.index = index; this.ackRequired = !getAcknowledgeMode().isAutoAck() && !getAcknowledgeMode().isManual(); - if (channel instanceof ChannelProxy) { - this.targetChannel = ((ChannelProxy) channel).getTargetChannel(); + if (channel instanceof ChannelProxy proxy) { + this.targetChannel = proxy.getTargetChannel(); } else { this.targetChannel = null; @@ -983,11 +1059,11 @@ public String getConsumerTag() { } /** - * Return the current epoch for this consumer; consumersMonitor must be held. + * Return the current epoch for this consumer; consumersLock must be held. * @return the epoch. */ int getEpoch() { - return this.epoch; + return this.epoch.get(); } /** @@ -1017,18 +1093,22 @@ boolean targetChanged() { } /** - * Increment and return the current epoch for this consumer; consumersMonitor must + * Increment and return the current epoch for this consumer; consumersLock must * be held. * @return the epoch. */ int incrementAndGetEpoch() { - return ++this.epoch; + return this.epoch.incrementAndGet(); } @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) { + if (!getChannel().isOpen()) { + this.logger.debug("Discarding prefetch, channel closed"); + return; + } MessageProperties messageProperties = getMessagePropertiesConverter().toMessageProperties(properties, envelope, "UTF-8"); messageProperties.setConsumerTag(consumerTag); @@ -1048,9 +1128,9 @@ public void handleDelivery(String consumerTag, Envelope envelope, try { executeListenerInTransaction(data, deliveryTag); } - catch (WrappedTransactionException e) { - if (e.getCause() instanceof Error) { - throw (Error) e.getCause(); + catch (WrappedTransactionException ex) { + if (ex.getCause() instanceof Error error) { + throw error; } } catch (Exception e) { @@ -1070,8 +1150,12 @@ public void handleDelivery(String consumerTag, Envelope envelope, // NOSONAR } } + if (DirectMessageListenerContainer.this.stopNow.get()) { + closeChannel(); + } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void executeListenerInTransaction(Object data, long deliveryTag) { if (this.isRabbitTxManager) { ConsumerChannelRegistry.registerConsumerChannel(getChannel(), this.connectionFactory); @@ -1168,14 +1252,18 @@ private void handleAck(long deliveryTag, boolean channelLocallyTransacted) { try { if (this.ackRequired) { if (this.messagesPerAck > 1) { - synchronized (this) { + this.lock.lock(); + try { this.latestDeferredDeliveryTag = deliveryTag; this.pendingAcks++; ackIfNecessary(this.lastAck); } + finally { + this.lock.unlock(); + } } else if (!isChannelTransacted() || isLocallyTransacted) { - getChannel().basicAck(deliveryTag, false); + sendAckWithNotify(deliveryTag, false); } } if (isLocallyTransacted) { @@ -1194,7 +1282,7 @@ else if (!isChannelTransacted() || isLocallyTransacted) { * @param now the current time. * @throws IOException if one occurs. */ - synchronized void ackIfNecessary(long now) throws IOException { + void ackIfNecessary(long now) throws Exception { // NOSONAR if (this.pendingAcks >= this.messagesPerAck || ( this.pendingAcks > 0 && (now - this.lastAck > this.ackTimeout || this.canceled))) { sendAck(now); @@ -1208,16 +1296,20 @@ private void rollback(long deliveryTag, Exception e) { if (this.ackRequired || ContainerUtils.isRejectManual(e)) { try { if (this.messagesPerAck > 1) { - synchronized (this) { + this.lock.lock(); + try { if (this.pendingAcks > 0) { sendAck(System.currentTimeMillis()); } } + finally { + this.lock.unlock(); + } } - getChannel().basicNack(deliveryTag, true, + getChannel().basicNack(deliveryTag, !isAsyncReplies(), ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), e, this.logger)); } - catch (IOException e1) { + catch (Exception e1) { this.logger.error("Failed to nack message", e1); } } @@ -1226,12 +1318,46 @@ private void rollback(long deliveryTag, Exception e) { } } - protected synchronized void sendAck(long now) throws IOException { - getChannel().basicAck(this.latestDeferredDeliveryTag, true); + void sendAck(long now) throws Exception { // NOSONAR + sendAckWithNotify(this.latestDeferredDeliveryTag, true); this.lastAck = now; this.pendingAcks = 0; } + /** + * Send ack and notify MessageAckListener(if set). + * @param deliveryTag DeliveryTag of this ack. + * @param multiple Whether multiple ack. + * @throws Exception Occurred when ack. + * @since 2.4.6 + */ + private void sendAckWithNotify(long deliveryTag, boolean multiple) throws Exception { // NOSONAR + try { + getChannel().basicAck(deliveryTag, multiple); + notifyMessageAckListener(true, deliveryTag, null); + } + catch (Exception e) { + notifyMessageAckListener(false, deliveryTag, e); + throw e; + } + } + + /** + * Notify MessageAckListener set on message listener. + * @param success Whether ack succeeded. + * @param deliveryTag The deliveryTag of ack. + * @param cause If an exception occurs. + * @since 2.4.6 + */ + private void notifyMessageAckListener(boolean success, long deliveryTag, @Nullable Throwable cause) { + try { + getMessageAckListener().onComplete(success, deliveryTag, cause); + } + catch (Exception e) { + this.logger.error("An exception occurred on MessageAckListener.", e); + } + } + @Override public void handleConsumeOk(String consumerTag) { super.handleConsumeOk(consumerTag); @@ -1259,23 +1385,32 @@ public void handleCancel(String consumerTag) { void cancelConsumer(final String eventMessage) { publishConsumerFailedEvent(eventMessage, true, null); - synchronized (DirectMessageListenerContainer.this.consumersMonitor) { - List list = DirectMessageListenerContainer.this.consumersByQueue.get(this.queue); + DirectMessageListenerContainer.this.consumersLock.lock(); + try { + List<@Nullable SimpleConsumer> list = + DirectMessageListenerContainer.this.consumersByQueue.get(this.queue); if (list != null) { list.remove(this); } DirectMessageListenerContainer.this.consumers.remove(this); addConsumerToRestart(this); } + finally { + DirectMessageListenerContainer.this.consumersLock.unlock(); + } finalizeConsumer(); } private void finalizeConsumer() { + closeChannel(); + consumerRemoved(this); + } + + private void closeChannel() { RabbitUtils.setPhysicalCloseRequired(getChannel(), true); RabbitUtils.closeChannel(getChannel()); RabbitUtils.closeConnection(this.connection); DirectMessageListenerContainer.this.cancellationLock.release(this); - consumerRemoved(this); } @Override @@ -1284,7 +1419,7 @@ public int hashCode() { int result = 1; result = prime * result + getEnclosingInstance().hashCode(); result = prime * result + this.index; - result = prime * result + ((this.queue == null) ? 0 : this.queue.hashCode()); + result = prime * result + this.queue.hashCode(); return result; } @@ -1306,15 +1441,7 @@ public boolean equals(Object obj) { if (this.index != other.index) { return false; } - if (this.queue == null) { - if (other.queue != null) { - return false; - } - } - else if (!this.queue.equals(other.queue)) { - return false; - } - return true; + return this.queue.equals(other.queue); } private DirectMessageListenerContainer getEnclosingInstance() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java index 7819f4d202..2826493aee 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author 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,19 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import com.rabbitmq.client.Channel; - /** * Listener container for Direct ReplyTo only listens to the pseudo queue * {@link Address#AMQ_RABBITMQ_REPLY_TO}. Consumers are added on-demand and @@ -47,8 +49,9 @@ public class DirectReplyToMessageListenerContainer extends DirectMessageListener private final ConcurrentMap whenUsed = new ConcurrentHashMap<>(); - private int consumerCount; + private final AtomicInteger consumerCount = new AtomicInteger(); + @SuppressWarnings("this-escape") public DirectReplyToMessageListenerContainer(ConnectionFactory connectionFactory) { super(connectionFactory); super.setQueueNames(Address.AMQ_RABBITMQ_REPLY_TO); @@ -84,10 +87,10 @@ public final boolean removeQueueNames(String... queueNames) { @Override public void setMessageListener(MessageListener messageListener) { - if (messageListener instanceof ChannelAwareMessageListener) { + if (messageListener instanceof ChannelAwareMessageListener channelAwareMessageListener) { super.setMessageListener((ChannelAwareMessageListener) (message, channel) -> { try { - ((ChannelAwareMessageListener) messageListener).onMessage(message, channel); + channelAwareMessageListener.onMessage(message, channel); } finally { this.inUseConsumerChannels.remove(channel); @@ -109,7 +112,7 @@ public void setMessageListener(MessageListener messageListener) { @Override protected void doStart() { if (!isRunning()) { - this.consumerCount = 0; + this.consumerCount.set(0); super.setConsumersPerQueue(0); super.doStart(); } @@ -118,18 +121,23 @@ protected void doStart() { @Override protected void processMonitorTask() { long now = System.currentTimeMillis(); - synchronized (this.consumersMonitor) { - long reduce = this.consumers.stream() - .filter(c -> this.whenUsed.containsKey(c) && !this.inUseConsumerChannels.containsValue(c) - && this.whenUsed.get(c) < now - getIdleEventInterval()) - .count(); - if (reduce > 0) { - if (logger.isDebugEnabled()) { - logger.debug("Reducing idle consumes by " + reduce); - } - this.consumerCount = (int) Math.max(0, this.consumerCount - reduce); - super.setConsumersPerQueue(this.consumerCount); + long reduce; + this.consumersLock.lock(); + try { + reduce = this.consumers.stream() + .filter(c -> this.whenUsed.containsKey(c) && !this.inUseConsumerChannels.containsValue(c) + && this.whenUsed.get(c) < now - getIdleEventInterval()) + .count(); + } + finally { + this.consumersLock.unlock(); + } + if (reduce > 0) { + if (logger.isDebugEnabled()) { + logger.debug("Reducing idle consumes by " + reduce); } + super.setConsumersPerQueue( + this.consumerCount.updateAndGet((current) -> (int) Math.max(0, current - reduce))); } } @@ -155,12 +163,13 @@ protected void consumerRemoved(SimpleConsumer consumer) { * @return the channel holder. */ public ChannelHolder getChannelHolder() { - synchronized (this.consumersMonitor) { - ChannelHolder channelHolder = null; - while (channelHolder == null) { - if (!isRunning()) { - throw new IllegalStateException("Direct reply-to container is not running"); - } + ChannelHolder channelHolder = null; + while (channelHolder == null) { + if (!isRunning()) { + throw new IllegalStateException("Direct reply-to container is not running"); + } + this.consumersLock.lock(); + try { for (SimpleConsumer consumer : this.consumers) { Channel candidate = consumer.getChannel(); if (candidate.isOpen() && this.inUseConsumerChannels.putIfAbsent(candidate, consumer) == null) { @@ -170,13 +179,23 @@ public ChannelHolder getChannelHolder() { break; } } - if (channelHolder == null) { - this.consumerCount++; - super.setConsumersPerQueue(this.consumerCount); + } + finally { + this.consumersLock.unlock(); + } + if (channelHolder == null) { + try { + super.setConsumersPerQueue(this.consumerCount.incrementAndGet()); + } + catch (AmqpTimeoutException timeoutException) { + // Possibly No available channels in the cache, so come back to consumers + // iteration until existing is available + this.consumerCount.decrementAndGet(); } } - return channelHolder; } + return channelHolder; + } /** @@ -188,7 +207,8 @@ public ChannelHolder getChannelHolder() { * @param message a message to be included in the cancel event if cancelConsumer is true. */ public void releaseConsumerFor(ChannelHolder channelHolder, boolean cancelConsumer, @Nullable String message) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { SimpleConsumer consumer = this.inUseConsumerChannels.get(channelHolder.getChannel()); if (consumer != null && consumer.getEpoch() == channelHolder.getConsumerEpoch()) { this.inUseConsumerChannels.remove(channelHolder.getChannel()); @@ -198,6 +218,9 @@ public void releaseConsumerFor(ChannelHolder channelHolder, boolean cancelConsum } } } + finally { + this.consumersLock.unlock(); + } } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerFailedEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerFailedEvent.java index 3508d99c5a..a136e6e5cb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerFailedEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerFailedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,25 +16,31 @@ package org.springframework.amqp.rabbit.listener; +import java.io.Serial; + +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.event.AmqpEvent; -import org.springframework.lang.Nullable; /** * Published when a listener consumer fails. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.5 * */ public class ListenerContainerConsumerFailedEvent extends AmqpEvent { + @Serial private static final long serialVersionUID = -8122166328567190605L; - private final String reason; + private final @Nullable String reason; private final boolean fatal; - private final Throwable throwable; + private final @Nullable Throwable throwable; /** * Construct an instance with the provided arguments. @@ -43,7 +49,7 @@ public class ListenerContainerConsumerFailedEvent extends AmqpEvent { * @param throwable the throwable. * @param fatal true if the startup failure was fatal (will not be retried). */ - public ListenerContainerConsumerFailedEvent(Object source, String reason, + public ListenerContainerConsumerFailedEvent(Object source, @Nullable String reason, @Nullable Throwable throwable, boolean fatal) { super(source); this.reason = reason; @@ -51,7 +57,7 @@ public ListenerContainerConsumerFailedEvent(Object source, String reason, this.throwable = throwable; } - public String getReason() { + public @Nullable String getReason() { return this.reason; } @@ -59,7 +65,7 @@ public boolean isFatal() { return this.fatal; } - public Throwable getThrowable() { + public @Nullable Throwable getThrowable() { return this.throwable; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerTerminatedEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerTerminatedEvent.java index 1a392522f6..012c12262d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerTerminatedEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerTerminatedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or 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.amqp.rabbit.listener; +import java.io.Serial; + +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.event.AmqpEvent; /** @@ -27,21 +31,22 @@ */ public class ListenerContainerConsumerTerminatedEvent extends AmqpEvent { + @Serial private static final long serialVersionUID = -8122166328567190605L; - private final String reason; + private final @Nullable String reason; /** * Construct an instance with the provided arguments. * @param source the source container. * @param reason the reason. */ - public ListenerContainerConsumerTerminatedEvent(Object source, String reason) { + public ListenerContainerConsumerTerminatedEvent(Object source, @Nullable String reason) { super(source); this.reason = reason; } - public String getReason() { + public @Nullable String getReason() { return this.reason; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerIdleEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerIdleEvent.java index ec30cf19a7..d978ac15dd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerIdleEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerIdleEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author 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,17 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.event.AmqpEvent; -import org.springframework.lang.Nullable; /** * An event that is emitted when a container is idle if the container * is configured to do so. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6 * */ @@ -36,8 +39,7 @@ public class ListenerContainerIdleEvent extends AmqpEvent { private final long idleTime; - @Nullable - private final String listenerId; + private final @Nullable String listenerId; private final List queueNames; @@ -61,7 +63,7 @@ public long getIdleTime() { * @return the queue names. */ public String[] getQueueNames() { - return this.queueNames.toArray(new String[this.queueNames.size()]); + return this.queueNames.toArray(new String[0]); } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerFailedRuleBasedTransactionAttribute.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerFailedRuleBasedTransactionAttribute.java index 30061b0e74..a84e2d1c05 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerFailedRuleBasedTransactionAttribute.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerFailedRuleBasedTransactionAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ * Allows users to control rollback based on the actual cause. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6.6 * */ @@ -33,7 +35,7 @@ public class ListenerFailedRuleBasedTransactionAttribute extends RuleBasedTransa @Override public boolean rollbackOn(Throwable ex) { - if (ex instanceof ListenerExecutionFailedException) { + if (ex instanceof ListenerExecutionFailedException && ex.getCause() != null) { return super.rollbackOn(ex.getCause()); } else { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java new file mode 100644 index 0000000000..90d16e8d2d --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener; + +import org.jspecify.annotations.Nullable; + +/** + * A listener for message ack when using {@link org.springframework.amqp.core.AcknowledgeMode#AUTO}. + * + * @author Cao Weibo + * @author Gary Russell + * @author Artem Bilan + * + * @since 2.4.6 + */ +@FunctionalInterface +public interface MessageAckListener { + + /** + * Listener callback. + * @param success Whether ack succeed. + * @param deliveryTag The deliveryTag of ack. + * @param cause The cause of failed ack. + */ + void onComplete(boolean success, long deliveryTag, @Nullable Throwable cause); + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageListenerContainer.java index 52d24b2b6a..acbe69d6fe 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package org.springframework.amqp.rabbit.listener; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageListener; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.SmartLifecycle; -import org.springframework.lang.Nullable; /** * Internal abstraction used by the framework representing a message @@ -32,7 +33,7 @@ public interface MessageListenerContainer extends SmartLifecycle, InitializingBean { /** - * Setup the message listener to use. Throws an {@link IllegalArgumentException} + * Set up the message listener to use. Throws an {@link IllegalArgumentException} * if that message listener type is not supported. * @param messageListener the {@code object} to wrapped to the {@code MessageListener}. */ diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java index baea9e49ae..e9570f4cb0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,17 @@ import java.lang.reflect.Method; import java.util.Arrays; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.listener.adapter.BatchMessagingMessageListenerAdapter; import org.springframework.amqp.rabbit.listener.adapter.HandlerAdapter; import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; @@ -44,15 +47,17 @@ */ public class MethodRabbitListenerEndpoint extends AbstractRabbitListenerEndpoint { + @SuppressWarnings("NullAway.Init") private Object bean; - private Method method; + private @Nullable Method method; + @SuppressWarnings("NullAway.Init") private MessageHandlerMethodFactory messageHandlerMethodFactory; private boolean returnExceptions; - private RabbitListenerErrorHandler errorHandler; + private @Nullable RabbitListenerErrorHandler errorHandler; private AdapterProvider adapterProvider = new DefaultAdapterProvider(); @@ -76,7 +81,7 @@ public void setMethod(Method method) { this.method = method; } - public Method getMethod() { + public @Nullable Method getMethod() { return this.method; } @@ -113,7 +118,7 @@ public void setErrorHandler(RabbitListenerErrorHandler errorHandler) { /** * @return the messageHandlerMethodFactory */ - protected MessageHandlerMethodFactory getMessageHandlerMethodFactory() { + protected @Nullable MessageHandlerMethodFactory getMessageHandlerMethodFactory() { return this.messageHandlerMethodFactory; } @@ -128,9 +133,7 @@ public void setAdapterProvider(AdapterProvider adapterProvider) { @Override protected MessagingMessageListenerAdapter createMessageListener(MessageListenerContainer container) { - Assert.state(this.messageHandlerMethodFactory != null, - "Could not create message listener - MessageHandlerMethodFactory not set"); - MessagingMessageListenerAdapter messageListener = createMessageListenerInstance(); + MessagingMessageListenerAdapter messageListener = createMessageListenerInstance(getBatchListener()); messageListener.setHandlerAdapter(configureListenerAdapter(messageListener)); String replyToAddress = getDefaultReplyToAddress(); if (replyToAddress != null) { @@ -140,9 +143,7 @@ protected MessagingMessageListenerAdapter createMessageListener(MessageListenerC if (messageConverter != null) { messageListener.setMessageConverter(messageConverter); } - if (getBeanResolver() != null) { - messageListener.setBeanResolver(getBeanResolver()); - } + messageListener.setBeanResolver(getBeanResolver()); return messageListener; } @@ -152,22 +153,24 @@ protected MessagingMessageListenerAdapter createMessageListener(MessageListenerC * @return the handler adapter. */ protected HandlerAdapter configureListenerAdapter(MessagingMessageListenerAdapter messageListener) { + Method methodToUse = getMethod(); + Assert.notNull(methodToUse, "'method' must be provided"); InvocableHandlerMethod invocableHandlerMethod = - this.messageHandlerMethodFactory.createInvocableHandlerMethod(getBean(), getMethod()); + this.messageHandlerMethodFactory.createInvocableHandlerMethod(getBean(), methodToUse); return new HandlerAdapter(invocableHandlerMethod); } /** * Create an empty {@link MessagingMessageListenerAdapter} instance. + * @param batch whether this endpoint is for a batch listener. * @return the {@link MessagingMessageListenerAdapter} instance. */ - protected MessagingMessageListenerAdapter createMessageListenerInstance() { - return this.adapterProvider.getAdapter(isBatchListener(), this.bean, this.method, this.returnExceptions, - this.errorHandler, getBatchingStrategy()); + protected MessagingMessageListenerAdapter createMessageListenerInstance(@Nullable Boolean batch) { + return this.adapterProvider.getAdapter(batch == null ? isBatchListener() : batch, this.bean, this.method, + this.returnExceptions, this.errorHandler, getBatchingStrategy()); } - @Nullable - private String getDefaultReplyToAddress() { + private @Nullable String getDefaultReplyToAddress() { Method listenerMethod = getMethod(); if (listenerMethod != null) { SendTo ann = AnnotationUtils.getAnnotation(listenerMethod, SendTo.class); @@ -183,16 +186,19 @@ private String getDefaultReplyToAddress() { return null; } - private String resolveSendTo(String value) { - if (getBeanFactory() != null) { - String resolvedValue = getBeanExpressionContext().getBeanFactory().resolveEmbeddedValue(value); - Object newValue = getResolver().evaluate(resolvedValue, getBeanExpressionContext()); - Assert.isInstanceOf(String.class, newValue, "Invalid @SendTo expression"); - return (String) newValue; - } - else { - return value; + private @Nullable String resolveSendTo(String value) { + BeanExpressionContext beanExpressionContext = getBeanExpressionContext(); + if (beanExpressionContext != null) { + String resolvedValue = beanExpressionContext.getBeanFactory().resolveEmbeddedValue(value); + BeanExpressionResolver resolverToUse = getResolver(); + if (resolverToUse != null) { + Object newValue = resolverToUse.evaluate(resolvedValue, beanExpressionContext); + Assert.isInstanceOf(String.class, newValue, "Invalid @SendTo expression"); + return (String) newValue; + } } + + return value; } @Override @@ -219,15 +225,17 @@ public interface AdapterProvider { * @param batchingStrategy the batching strategy for batch listeners. * @return the adapter. */ - MessagingMessageListenerAdapter getAdapter(boolean batch, Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy); + MessagingMessageListenerAdapter getAdapter(boolean batch, @Nullable Object bean, @Nullable Method method, + boolean returnExceptions, @Nullable RabbitListenerErrorHandler errorHandler, + @Nullable BatchingStrategy batchingStrategy); + } private static final class DefaultAdapterProvider implements AdapterProvider { @Override - public MessagingMessageListenerAdapter getAdapter(boolean batch, Object bean, Method method, - boolean returnExceptions, RabbitListenerErrorHandler errorHandler, + public MessagingMessageListenerAdapter getAdapter(boolean batch, @Nullable Object bean, @Nullable Method method, + boolean returnExceptions, @Nullable RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) { if (batch) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java new file mode 100644 index 0000000000..eb70aa8968 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java @@ -0,0 +1,115 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.Timer.Builder; +import io.micrometer.core.instrument.Timer.Sample; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Abstraction to avoid hard reference to Micrometer. + * + * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan + * + * @since 2.4.6 + * + */ +public final class MicrometerHolder { + + private final ConcurrentMap timers = new ConcurrentHashMap<>(); + + @SuppressWarnings("NullAway.Init") + private final MeterRegistry registry; + + private final Map tags; + + private final String listenerId; + + @SuppressWarnings("NullAway") // Dataflow analysis limitation + MicrometerHolder(@Nullable ApplicationContext context, String listenerId, Map tags) { + Assert.notNull(context, "'context' must not be null"); + try { + this.registry = context.getBeanProvider(MeterRegistry.class).getIfUnique(); + } + catch (NoUniqueBeanDefinitionException ex) { + throw new IllegalStateException(ex); + } + if (this.registry != null) { + this.listenerId = listenerId; + this.tags = tags; + } + else { + throw new IllegalStateException("No micrometer registry present (or more than one and " + + "there is not exactly one marked with @Primary)"); + } + } + + public Object start() { + return Timer.start(this.registry); + } + + public void success(Object sample, String queue) { + Timer timer = this.timers.get(queue + "none"); + if (timer == null) { + timer = buildTimer(this.listenerId, "success", queue, "none"); + } + ((Sample) sample).stop(timer); + } + + public void failure(Object sample, String queue, String exception) { + Timer timer = this.timers.get(queue + exception); + if (timer == null) { + timer = buildTimer(this.listenerId, "failure", queue, exception); + } + ((Sample) sample).stop(timer); + } + + private Timer buildTimer(String aListenerId, String result, String queue, String exception) { + + Builder builder = Timer.builder("spring.rabbitmq.listener") + .description("Spring RabbitMQ Listener") + .tag("listener.id", aListenerId) + .tag("queue", queue) + .tag("result", result) + .tag("exception", exception); + if (!CollectionUtils.isEmpty(this.tags)) { + this.tags.forEach(builder::tag); + } + Timer registeredTimer = builder.register(this.registry); + this.timers.put(queue + exception, registeredTimer); + return registeredTimer; + } + + void destroy() { + this.timers.values().forEach(this.registry::remove); + this.timers.clear(); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MissingQueueEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MissingQueueEvent.java index e02437874e..df09860c1a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MissingQueueEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MissingQueueEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or 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.amqp.rabbit.listener; +import java.io.Serial; + import org.springframework.amqp.event.AmqpEvent; /** @@ -27,6 +29,7 @@ */ public class MissingQueueEvent extends AmqpEvent { + @Serial private static final long serialVersionUID = 1L; private final String queue; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java index 8a57710aff..c1c578fef1 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-2025 the original author 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,15 +20,21 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.listener.adapter.DelegatingInvocableHandler; import org.springframework.amqp.rabbit.listener.adapter.HandlerAdapter; import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; -import org.springframework.lang.Nullable; +import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.Assert; import org.springframework.validation.Validator; /** * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan + * * @since 1.5 * */ @@ -36,20 +42,9 @@ public class MultiMethodRabbitListenerEndpoint extends MethodRabbitListenerEndpo private final List methods; - private final Method defaultMethod; + private final @Nullable Method defaultMethod; - private Validator validator; - - /** - * Construct an instance for the provided methods and bean. - * @param methods the methods. - * @param bean the bean. - * @deprecated - no longer used. - */ - @Deprecated - public MultiMethodRabbitListenerEndpoint(List methods, Object bean) { - this(methods, null, bean); - } + private @Nullable Validator validator; /** * Construct an instance for the provided methods, default method and bean. @@ -58,6 +53,7 @@ public MultiMethodRabbitListenerEndpoint(List methods, Object bean) { * @param bean the bean. * @since 2.0.3 */ + @SuppressWarnings("this-escape") public MultiMethodRabbitListenerEndpoint(List methods, @Nullable Method defaultMethod, Object bean) { this.methods = methods; this.defaultMethod = defaultMethod; @@ -75,11 +71,15 @@ public void setValidator(Validator validator) { @Override protected HandlerAdapter configureListenerAdapter(MessagingMessageListenerAdapter messageListener) { - List invocableHandlerMethods = new ArrayList(); + List invocableHandlerMethods = new ArrayList<>(); InvocableHandlerMethod defaultHandler = null; + MessageHandlerMethodFactory messageHandlerMethodFactory = getMessageHandlerMethodFactory(); + Assert.state(messageHandlerMethodFactory != null, + "Could not create message listener - MessageHandlerMethodFactory not set"); + Object beanToUse = getBean(); for (Method method : this.methods) { - InvocableHandlerMethod handler = getMessageHandlerMethodFactory() - .createInvocableHandlerMethod(getBean(), method); + InvocableHandlerMethod handler = messageHandlerMethodFactory + .createInvocableHandlerMethod(beanToUse, method); invocableHandlerMethods.add(handler); if (method.equals(this.defaultMethod)) { defaultHandler = handler; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java new file mode 100644 index 0000000000..c8b2925c75 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java @@ -0,0 +1,163 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener; + +import java.util.HashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.rabbit.connection.RabbitAccessor; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.ClassUtils; + +/** + * @author Gary Russell + * @author Artem Bilan + * + * @since 3.0.5 + * + */ +public abstract class ObservableListenerContainer extends RabbitAccessor + implements MessageListenerContainer, ApplicationContextAware, BeanNameAware, DisposableBean { + + private static final boolean MICROMETER_PRESENT = ClassUtils.isPresent( + "io.micrometer.core.instrument.MeterRegistry", AbstractMessageListenerContainer.class.getClassLoader()); + + private @Nullable ApplicationContext applicationContext; + + private final Map micrometerTags = new HashMap<>(); + + private @Nullable MicrometerHolder micrometerHolder; + + private boolean micrometerEnabled = true; + + private boolean observationEnabled = false; + + private String beanName = "not.a.Spring.bean"; + + private @Nullable String listenerId; + + protected final @Nullable ApplicationContext getApplicationContext() { + return this.applicationContext; + } + + @Override + public final void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + protected @Nullable MicrometerHolder getMicrometerHolder() { + return this.micrometerHolder; + } + + /** + * Set additional tags for the Micrometer listener timers. + * @param tags the tags. + * @since 2.2 + */ + public void setMicrometerTags(@Nullable Map tags) { + if (tags != null) { + this.micrometerTags.putAll(tags); + } + } + + /** + * Set to {@code false} to disable micrometer listener timers. When true, ignored + * if {@link #setObservationEnabled(boolean)} is set to true. + * @param micrometerEnabled false to disable. + * @since 2.2 + * @see #setObservationEnabled(boolean) + */ + public void setMicrometerEnabled(boolean micrometerEnabled) { + this.micrometerEnabled = micrometerEnabled; + } + + /** + * Enable observation via micrometer; disables basic Micrometer timers enabled + * by {@link #setMicrometerEnabled(boolean)}. + * @param observationEnabled true to enable. + * @since 3.0 + * @see #setMicrometerEnabled(boolean) + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + protected void checkMicrometer() { + try { + if (this.micrometerHolder == null && MICROMETER_PRESENT && this.micrometerEnabled + && !this.observationEnabled && this.applicationContext != null) { + + this.micrometerHolder = new MicrometerHolder(this.applicationContext, getListenerId(), + this.micrometerTags); + } + } + catch (IllegalStateException e) { + this.logger.debug("Could not enable micrometer timers", e); + } + } + + protected void checkObservation() { + if (this.observationEnabled) { + obtainObservationRegistry(this.applicationContext); + } + } + + + protected boolean isApplicationContextClosed() { + return this.applicationContext instanceof ConfigurableApplicationContext configurableCtx + && configurableCtx.isClosed(); + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + /** + * @return The bean name that this listener container has been assigned in its containing bean factory, if any. + */ + protected final String getBeanName() { + return this.beanName; + } + + /** + * The 'id' attribute of the listener. + * @return the id (or the container bean name if no id set). + */ + public String getListenerId() { + return this.listenerId != null ? this.listenerId : this.beanName; + } + + @Override + public void setListenerId(String listenerId) { + this.listenerId = listenerId; + } + + @Override + public void destroy() { + if (this.micrometerHolder != null) { + this.micrometerHolder.destroy(); + } + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerContainerFactory.java index ebb7b82832..ce8068f25f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,21 @@ package org.springframework.amqp.rabbit.listener; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanNameAware; /** * Factory of {@link MessageListenerContainer}s. * @param the container type. * @author Stephane Nicoll * @author Gary Russell + * @author Ngoc Nhan * @since 1.4 * @see RabbitListenerEndpoint */ @FunctionalInterface -public interface RabbitListenerContainerFactory { +public interface RabbitListenerContainerFactory extends BeanNameAware { /** * Create a {@link MessageListenerContainer} for the given @@ -48,4 +51,19 @@ default C createListenerContainer() { return createListenerContainer(null); } + @Override + default void setBeanName(String name) { + + } + + /** + * Return a bean name of the component or null if not a bean. + * @return the bean name. + * @since 3.2 + */ + @Nullable + default String getBeanName() { + return null; + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java index 979dd9ee1c..284aace533 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,13 @@ package org.springframework.amqp.rabbit.listener; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.core.task.TaskExecutor; -import org.springframework.lang.Nullable; /** * Model for a Rabbit listener endpoint. Can be used against a @@ -40,18 +41,21 @@ public interface RabbitListenerEndpoint { * container. * @see RabbitListenerContainerFactory#createListenerContainer */ + @Nullable String getId(); /** * @return the group of this endpoint or null if not in a group. * @since 1.5 */ + @Nullable String getGroup(); /** * @return the concurrency of this endpoint. * @since 2.0 */ + @Nullable String getConcurrency(); /** @@ -59,10 +63,11 @@ public interface RabbitListenerEndpoint { * @return the autoStartup. * @since 2.0 */ + @Nullable Boolean getAutoStartup(); /** - * Setup the specified message listener container with the model + * Set up the specified message listener container with the model * defined by this endpoint. *

This endpoint must provide the requested missing option(s) of * the specified container to make it usable. Usually, this is about @@ -92,8 +97,7 @@ default void setMessageConverter(MessageConverter converter) { * @return the converter. * @since 2.0.8 */ - @Nullable - default MessageConverter getMessageConverter() { + default @Nullable MessageConverter getMessageConverter() { return null; } @@ -103,8 +107,7 @@ default MessageConverter getMessageConverter() { * @return the executor. * @since 2.2 */ - @Nullable - default TaskExecutor getTaskExecutor() { + default @Nullable TaskExecutor getTaskExecutor() { return null; } @@ -114,9 +117,16 @@ default TaskExecutor getTaskExecutor() { * @since 2.2 */ default void setBatchListener(boolean batchListener) { - // NOSONAR empty } + /** + * Whether this endpoint is for a batch listener. + * @return {@link Boolean#TRUE} if batch. + * @since 3.0 + */ + @Nullable + Boolean getBatchListener(); + /** * Set a {@link BatchingStrategy} to use when debatching messages. * @param batchingStrategy the batching strategy. @@ -124,7 +134,15 @@ default void setBatchListener(boolean batchListener) { * @see #setBatchListener(boolean) */ default void setBatchingStrategy(BatchingStrategy batchingStrategy) { - // NOSONAR empty + } + + /** + * Return this endpoint's batching strategy, or null. + * @return the strategy. + * @since 2.4.7 + */ + default @Nullable BatchingStrategy getBatchingStrategy() { + return null; } /** @@ -132,8 +150,7 @@ default void setBatchingStrategy(BatchingStrategy batchingStrategy) { * @return the acknowledgment mode. * @since 2.2 */ - @Nullable - default AcknowledgeMode getAckMode() { + default @Nullable AcknowledgeMode getAckMode() { return null; } @@ -143,8 +160,7 @@ default AcknowledgeMode getAckMode() { * @return the post processor. * @since 2.2.5 */ - @Nullable - default ReplyPostProcessor getReplyPostProcessor() { + default @Nullable ReplyPostProcessor getReplyPostProcessor() { return null; } @@ -153,8 +169,7 @@ default ReplyPostProcessor getReplyPostProcessor() { * @return the content type. * @since 2.3 */ - @Nullable - default String getReplyContentType() { + default @Nullable String getReplyContentType() { return null; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java index 998c28957b..d0d8492259 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author 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,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.util.Assert; @@ -37,30 +40,33 @@ * @author Stephane Nicoll * @author Juergen Hoeller * @author Artem Bilan + * * @since 1.4 + * * @see org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer */ public class RabbitListenerEndpointRegistrar implements BeanFactoryAware, InitializingBean { - private final List endpointDescriptors = - new ArrayList(); + private final List endpointDescriptors = new ArrayList<>(); + + private final Lock endpointDescriptorsLock = new ReentrantLock(); private List customMethodArgumentResolvers = new ArrayList<>(); - @Nullable - private RabbitListenerEndpointRegistry endpointRegistry; + private @Nullable RabbitListenerEndpointRegistry endpointRegistry; - private MessageHandlerMethodFactory messageHandlerMethodFactory; + private @Nullable MessageHandlerMethodFactory messageHandlerMethodFactory; - private RabbitListenerContainerFactory containerFactory; + private @Nullable RabbitListenerContainerFactory containerFactory; - private String containerFactoryBeanName; + private @Nullable String containerFactoryBeanName; + @SuppressWarnings("NullAway.Init") private BeanFactory beanFactory; private boolean startImmediately; - private Validator validator; + private @Nullable Validator validator; /** * Set the {@link RabbitListenerEndpointRegistry} instance to use. @@ -74,8 +80,7 @@ public void setEndpointRegistry(RabbitListenerEndpointRegistry endpointRegistry) * @return the {@link RabbitListenerEndpointRegistry} instance for this * registrar, may be {@code null}. */ - @Nullable - public RabbitListenerEndpointRegistry getEndpointRegistry() { + public @Nullable RabbitListenerEndpointRegistry getEndpointRegistry() { return this.endpointRegistry; } @@ -88,7 +93,6 @@ public List getCustomMethodArgumentResolvers() { return Collections.unmodifiableList(this.customMethodArgumentResolvers); } - /** * Add custom methods arguments resolvers to * {@link org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor} @@ -106,22 +110,21 @@ public void setCustomMethodArgumentResolvers(HandlerMethodArgumentResolver... me *

* By default, * {@link org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory} - * is used and it can be configured further to support additional method arguments or + * is used, and it can be configured further to support additional method arguments or * to customize conversion and validation support. See * {@link org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory} * javadoc for more details. * @param rabbitHandlerMethodFactory the {@link MessageHandlerMethodFactory} instance. */ public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory rabbitHandlerMethodFactory) { - Assert.isNull(this.validator, - "A validator cannot be provided with a custom message handler factory"); + Assert.isNull(this.validator, "A validator cannot be provided with a custom message handler factory"); this.messageHandlerMethodFactory = rabbitHandlerMethodFactory; } /** * @return the custom {@link MessageHandlerMethodFactory} to use, if any. */ - public MessageHandlerMethodFactory getMessageHandlerMethodFactory() { + public @Nullable MessageHandlerMethodFactory getMessageHandlerMethodFactory() { return this.messageHandlerMethodFactory; } @@ -163,8 +166,7 @@ public void setBeanFactory(BeanFactory beanFactory) { * @return the validator. * @since 2.3.7 */ - @Nullable - public Validator getValidator() { + public @Nullable Validator getValidator() { return this.validator; } @@ -186,16 +188,20 @@ public void afterPropertiesSet() { protected void registerAllEndpoints() { Assert.state(this.endpointRegistry != null, "No registry available"); - synchronized (this.endpointDescriptors) { + this.endpointDescriptorsLock.lock(); + try { for (AmqpListenerEndpointDescriptor descriptor : this.endpointDescriptors) { - if (descriptor.endpoint instanceof MultiMethodRabbitListenerEndpoint && this.validator != null) { - ((MultiMethodRabbitListenerEndpoint) descriptor.endpoint).setValidator(this.validator); + if (descriptor.endpoint instanceof MultiMethodRabbitListenerEndpoint multi && this.validator != null) { + multi.setValidator(this.validator); } this.endpointRegistry.registerListenerContainer(// NOSONAR never null descriptor.endpoint, resolveContainerFactory(descriptor)); } this.startImmediately = true; // trigger immediate startup } + finally { + this.endpointDescriptorsLock.unlock(); + } } private RabbitListenerContainerFactory resolveContainerFactory(AmqpListenerEndpointDescriptor descriptor) { @@ -206,9 +212,8 @@ else if (this.containerFactory != null) { return this.containerFactory; } else if (this.containerFactoryBeanName != null) { - Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name"); - this.containerFactory = this.beanFactory.getBean( - this.containerFactoryBeanName, RabbitListenerContainerFactory.class); + this.containerFactory = + this.beanFactory.getBean(this.containerFactoryBeanName, RabbitListenerContainerFactory.class); return this.containerFactory; // Consider changing this if live change of the factory is required } else { @@ -226,14 +231,17 @@ else if (this.containerFactoryBeanName != null) { * @param endpoint the {@link RabbitListenerEndpoint} instance to register. * @param factory the {@link RabbitListenerContainerFactory} to use. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void registerEndpoint(RabbitListenerEndpoint endpoint, @Nullable RabbitListenerContainerFactory factory) { + Assert.notNull(endpoint, "Endpoint must be set"); Assert.hasText(endpoint.getId(), "Endpoint id must be set"); Assert.state(!this.startImmediately || this.endpointRegistry != null, "No registry available"); // Factory may be null, we defer the resolution right before actually creating the container AmqpListenerEndpointDescriptor descriptor = new AmqpListenerEndpointDescriptor(endpoint, factory); - synchronized (this.endpointDescriptors) { + this.endpointDescriptorsLock.lock(); + try { if (this.startImmediately) { // Register and start immediately this.endpointRegistry.registerListenerContainer(descriptor.endpoint, // NOSONAR never null resolveContainerFactory(descriptor), true); @@ -242,6 +250,9 @@ public void registerEndpoint(RabbitListenerEndpoint endpoint, this.endpointDescriptors.add(descriptor); } } + finally { + this.endpointDescriptorsLock.unlock(); + } } /** @@ -255,18 +266,8 @@ public void registerEndpoint(RabbitListenerEndpoint endpoint) { registerEndpoint(endpoint, null); } - - private static final class AmqpListenerEndpointDescriptor { - - private final RabbitListenerEndpoint endpoint; - - private final RabbitListenerContainerFactory containerFactory; - - AmqpListenerEndpointDescriptor(RabbitListenerEndpoint endpoint, - @Nullable RabbitListenerContainerFactory containerFactory) { - this.endpoint = endpoint; - this.containerFactory = containerFactory; - } + private record AmqpListenerEndpointDescriptor(RabbitListenerEndpoint endpoint, + @Nullable RabbitListenerContainerFactory containerFactory) { } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java index 5b20169ffa..e0dcdede30 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author 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 java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; @@ -36,7 +39,6 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.SmartLifecycle; import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -57,7 +59,9 @@ * @author Juergen Hoeller * @author Artem Bilan * @author Gary Russell + * * @since 1.4 + * * @see RabbitListenerEndpoint * @see MessageListenerContainer * @see RabbitListenerContainerFactory @@ -67,20 +71,20 @@ public class RabbitListenerEndpointRegistry implements DisposableBean, SmartLife protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR protected - private final Map listenerContainers = - new ConcurrentHashMap(); + private final Map listenerContainers = new ConcurrentHashMap<>(); + + private final Lock listenerContainersLock = new ReentrantLock(); private int phase = Integer.MAX_VALUE; - private ConfigurableApplicationContext applicationContext; + private @Nullable ConfigurableApplicationContext applicationContext; private boolean contextRefreshed; - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - if (applicationContext instanceof ConfigurableApplicationContext) { - this.applicationContext = (ConfigurableApplicationContext) applicationContext; + if (applicationContext instanceof ConfigurableApplicationContext configurable) { + this.applicationContext = configurable; } } @@ -92,7 +96,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws * @see RabbitListenerEndpoint#getId() * @see #getListenerContainerIds() */ - public MessageListenerContainer getListenerContainer(String id) { + public @Nullable MessageListenerContainer getListenerContainer(String id) { Assert.hasText(id, "Container identifier must not be empty"); return this.listenerContainers.get(id); } @@ -116,8 +120,7 @@ public Collection getListenerContainers() { /** * Create a message listener container for the given {@link RabbitListenerEndpoint}. - *

This create the necessary infrastructure to honor that endpoint - * with regards to its configuration. + *

This create the necessary infrastructure to honor that endpoint in regard to its configuration. * @param endpoint the endpoint to add * @param factory the listener factory to use * @see #registerListenerContainer(RabbitListenerEndpoint, RabbitListenerContainerFactory, boolean) @@ -128,8 +131,7 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis /** * Create a message listener container for the given {@link RabbitListenerEndpoint}. - *

This create the necessary infrastructure to honor that endpoint - * with regards to its configuration. + *

This create the necessary infrastructure to honor that endpoint in regard to its configuration. *

The {@code startImmediately} flag determines if the container should be * started immediately. * @param endpoint the endpoint to add. @@ -138,15 +140,17 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis * @see #getListenerContainers() * @see #getListenerContainer(String) */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) // Dataflow analysis limitation public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitListenerContainerFactory factory, - boolean startImmediately) { + boolean startImmediately) { + Assert.notNull(endpoint, "Endpoint must not be null"); Assert.notNull(factory, "Factory must not be null"); String id = endpoint.getId(); Assert.hasText(id, "Endpoint id must not be empty"); - synchronized (this.listenerContainers) { + this.listenerContainersLock.lock(); + try { Assert.state(!this.listenerContainers.containsKey(id), "Another endpoint is already registered with id '" + id + "'"); MessageListenerContainer container = createListenerContainer(endpoint, factory); @@ -157,7 +161,7 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis containerGroup = this.applicationContext.getBean(endpoint.getGroup(), List.class); } else { - containerGroup = new ArrayList(); + containerGroup = new ArrayList<>(); this.applicationContext.getBeanFactory().registerSingleton(endpoint.getGroup(), containerGroup); } containerGroup.add(container); @@ -169,6 +173,9 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis startIfNecessary(container); } } + finally { + this.listenerContainersLock.unlock(); + } } /** @@ -209,9 +216,9 @@ public MessageListenerContainer unregisterListenerContainer(String id) { @Override public void destroy() { for (MessageListenerContainer listenerContainer : getListenerContainers()) { - if (listenerContainer instanceof DisposableBean) { + if (listenerContainer instanceof DisposableBean disposable) { try { - ((DisposableBean) listenerContainer).destroy(); + disposable.destroy(); } catch (Exception ex) { this.logger.warn("Failed to destroy listener container [" + listenerContainer + "]", ex); @@ -220,7 +227,6 @@ public void destroy() { } } - // Delegating implementation of SmartLifecycle @Override @@ -250,7 +256,7 @@ public void stop() { @Override public void stop(Runnable callback) { Collection containers = getListenerContainers(); - if (containers.size() > 0) { + if (!containers.isEmpty()) { AggregatingCallback aggregatingCallback = new AggregatingCallback(containers.size(), callback); for (MessageListenerContainer listenerContainer : containers) { try { @@ -290,7 +296,6 @@ private void startIfNecessary(MessageListenerContainer listenerContainer) { } } - @Override public void onApplicationEvent(ContextRefreshedEvent event) { if (event.getApplicationContext().equals(this.applicationContext)) { @@ -298,7 +303,6 @@ public void onApplicationEvent(ContextRefreshedEvent event) { } } - private static final class AggregatingCallback implements Runnable { private final AtomicInteger count; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index b77ec1c17e..d074e5e98b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -31,6 +30,11 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.PossibleAuthenticationFailureException; +import com.rabbitmq.client.ShutdownSignalException; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpException; @@ -41,12 +45,15 @@ import org.springframework.amqp.core.BatchMessageListener; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils; import org.springframework.amqp.rabbit.connection.ConsumerChannelRegistry; import org.springframework.amqp.rabbit.connection.RabbitResourceHolder; import org.springframework.amqp.rabbit.connection.RabbitUtils; +import org.springframework.amqp.rabbit.connection.RoutingConnectionFactory; import org.springframework.amqp.rabbit.connection.SimpleResourceHolder; import org.springframework.amqp.rabbit.listener.api.ChannelAwareBatchMessageListener; import org.springframework.amqp.rabbit.listener.exception.FatalListenerExecutionException; @@ -57,19 +64,16 @@ import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.support.ConsumerTagStrategy; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.log.LogMessage; import org.springframework.jmx.export.annotation.ManagedMetric; import org.springframework.jmx.support.MetricType; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.Assert; import org.springframework.util.backoff.BackOffExecution; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.PossibleAuthenticationFailureException; -import com.rabbitmq.client.ShutdownSignalException; - /** * @author Mark Pollack * @author Mark Fisher @@ -77,6 +81,12 @@ * @author Gary Russell * @author Artem Bilan * @author Alex Panchenko + * @author Mat Jaggard + * @author Yansong Ren + * @author Tim Bourquin + * @author Jeonggi Kim + * @author Java4ye + * @author Thomas Badie * * @since 1.0 */ @@ -98,7 +108,7 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta private final AtomicLong lastNoMessageAlert = new AtomicLong(); - private final AtomicReference containerStoppingForAbort = new AtomicReference<>(); + private final AtomicReference<@Nullable Thread> containerStoppingForAbort = new AtomicReference<>(); private final BlockingQueue abortEvents = new LinkedBlockingQueue<>(); @@ -118,19 +128,23 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta private long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT; - private Set consumers; + private long batchReceiveTimeout; + + private @Nullable Set consumers; - private Integer declarationRetries; + private @Nullable Integer declarationRetries; - private Long retryDeclarationInterval; + private @Nullable Long retryDeclarationInterval; - private TransactionTemplate transactionTemplate; + private @Nullable TransactionTemplate transactionTemplate; private long consumerStartTimeout = DEFAULT_CONSUMER_START_TIMEOUT; + private boolean enforceImmediateAckForManual; + private volatile int concurrentConsumers = 1; - private volatile Integer maxConcurrentConsumers; + private volatile @Nullable Integer maxConcurrentConsumers; private volatile long lastConsumerStarted; @@ -147,6 +161,7 @@ public SimpleMessageListenerContainer() { * * @param connectionFactory the {@link ConnectionFactory} */ + @SuppressWarnings("this-escape") public SimpleMessageListenerContainer(ConnectionFactory connectionFactory) { setConnectionFactory(connectionFactory); } @@ -164,11 +179,13 @@ public void setConcurrentConsumers(final int concurrentConsumers) { Assert.isTrue(concurrentConsumers > 0, "'concurrentConsumers' value must be at least 1 (one)"); Assert.isTrue(!isExclusive() || concurrentConsumers == 1, "When the consumer is exclusive, the concurrency must be 1"); - if (this.maxConcurrentConsumers != null) { - Assert.isTrue(concurrentConsumers <= this.maxConcurrentConsumers, + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; + if (maxConcurrentConsumersToCheck != null) { + Assert.isTrue(concurrentConsumers <= maxConcurrentConsumersToCheck, "'concurrentConsumers' cannot be more than 'maxConcurrentConsumers'"); } - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (logger.isDebugEnabled()) { logger.debug("Changing consumers from " + this.concurrentConsumers + " to " + concurrentConsumers); } @@ -178,6 +195,9 @@ public void setConcurrentConsumers(final int concurrentConsumers) { adjustConsumers(delta); } } + finally { + this.consumersLock.unlock(); + } } /** @@ -223,7 +243,6 @@ public void setConcurrency(String concurrency) { int maxConsumersToSet = Integer.parseInt(concurrency.substring(separatorIndex + 1)); Assert.isTrue(maxConsumersToSet >= consumersToSet, "'maxConcurrentConsumers' value must be at least 'concurrentConsumers'"); - this.concurrentConsumers = 1; this.maxConcurrentConsumers = null; setConcurrentConsumers(consumersToSet); setMaxConcurrentConsumers(maxConsumersToSet); @@ -244,8 +263,9 @@ public void setConcurrency(String concurrency) { */ @Override public final void setExclusive(boolean exclusive) { + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; Assert.isTrue(!exclusive || (this.concurrentConsumers == 1 - && (this.maxConcurrentConsumers == null || this.maxConcurrentConsumers == 1)), + && (maxConcurrentConsumersToCheck == null || maxConcurrentConsumersToCheck == 1)), "When the consumer is exclusive, the concurrency must be 1"); super.setExclusive(exclusive); } @@ -324,6 +344,19 @@ public void setReceiveTimeout(long receiveTimeout) { this.receiveTimeout = receiveTimeout; } + /** + * The number of milliseconds of timeout for gathering batch messages. + * It limits the time to wait to fill batchSize. + * Default is 0 (no timeout). + * @param batchReceiveTimeout the timeout for gathering batch messages. + * @since 3.1.2 + * @see #setBatchSize(int) + */ + public void setBatchReceiveTimeout(long batchReceiveTimeout) { + Assert.isTrue(batchReceiveTimeout >= 0, "'batchReceiveTimeout' must be >= 0"); + this.batchReceiveTimeout = batchReceiveTimeout; + } + /** * This property has several functions. *

@@ -482,6 +515,18 @@ public void setConsumerStartTimeout(long consumerStartTimeout) { this.consumerStartTimeout = consumerStartTimeout; } + /** + * Set to {@code true} to enforce {@link Channel#basicAck(long, boolean)} + * for {@link org.springframework.amqp.core.AcknowledgeMode#MANUAL} + * when {@link ImmediateAcknowledgeAmqpException} is thrown. + * This might be a tentative solution to not break behavior for current minor version. + * @param enforceImmediateAckForManual the flag to ack message for MANUAL mode on ImmediateAcknowledgeAmqpException + * @since 3.1.2 + */ + public void setEnforceImmediateAckForManual(boolean enforceImmediateAckForManual) { + this.enforceImmediateAckForManual = enforceImmediateAckForManual; + } + /** * Avoid the possibility of not configuring the CachingConnectionFactory in sync with the number of concurrent * consumers. @@ -530,11 +575,12 @@ public int getActiveConsumerCount() { @Override protected void doStart() { Assert.state(!this.consumerBatchEnabled || getMessageListener() instanceof BatchMessageListener - || getMessageListener() instanceof ChannelAwareBatchMessageListener, + || getMessageListener() instanceof ChannelAwareBatchMessageListener, "When setting 'consumerBatchEnabled' to true, the listener must support batching"); checkListenerContainerAware(); super.doStart(); - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null) { throw new IllegalStateException("A stopped container should not have consumers"); } @@ -550,7 +596,7 @@ protected void doStart() { } return; } - Set processors = new HashSet(); + Set processors = new HashSet<>(); for (BlockingQueueConsumer consumer : this.consumers) { AsyncMessageProcessingConsumer processor = new AsyncMessageProcessingConsumer(consumer); processors.add(processor); @@ -561,23 +607,23 @@ protected void doStart() { } waitForConsumersToStart(processors); } + finally { + this.consumersLock.unlock(); + } } private void checkListenerContainerAware() { Object messageListener = getMessageListener(); - if (messageListener instanceof ListenerContainerAware) { - Collection expectedQueueNames = ((ListenerContainerAware) messageListener).expectedQueueNames(); + if (messageListener instanceof ListenerContainerAware containerAware) { + Collection expectedQueueNames = containerAware.expectedQueueNames(); if (expectedQueueNames != null) { String[] queueNames = getQueueNames(); Assert.state(expectedQueueNames.size() == queueNames.length, "Listener expects us to be listening on '" + expectedQueueNames + "'; our queues: " + Arrays.asList(queueNames)); - boolean found = false; + boolean found = true; for (String queueName : queueNames) { - if (expectedQueueNames.contains(queueName)) { - found = true; - } - else { + if (!expectedQueueNames.contains(queueName)) { found = false; break; } @@ -590,7 +636,7 @@ private void checkListenerContainerAware() { private void waitForConsumersToStart(Set processors) { for (AsyncMessageProcessingConsumer processor : processors) { - FatalListenerStartupException startupException = null; + FatalListenerStartupException startupException; try { startupException = processor.getStartupException(); } @@ -605,76 +651,112 @@ private void waitForConsumersToStart(Set process } @Override - protected void doShutdown() { + protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { Thread thread = this.containerStoppingForAbort.get(); if (thread != null && !thread.equals(Thread.currentThread())) { logger.info("Shutdown ignored - container is stopping due to an aborted consumer"); + runCallbackIfNotNull(callback); return; } + List canceledConsumers = new ArrayList<>(); + this.consumersLock.lock(); try { - List canceledConsumers = new ArrayList<>(); - synchronized (this.consumersMonitor) { - if (this.consumers != null) { - Iterator consumerIterator = this.consumers.iterator(); - while (consumerIterator.hasNext()) { - BlockingQueueConsumer consumer = consumerIterator.next(); + if (this.consumers != null) { + Iterator consumerIterator = this.consumers.iterator(); + if (isForceStop()) { + this.stopNow.set(true); + } + while (consumerIterator.hasNext()) { + BlockingQueueConsumer consumer = consumerIterator.next(); + if (!isForceStop()) { consumer.basicCancel(true); - canceledConsumers.add(consumer); - consumerIterator.remove(); - if (consumer.declaring) { - consumer.thread.interrupt(); - } } + canceledConsumers.add(consumer); + consumerIterator.remove(); + if (consumer.declaring) { + consumer.thread.interrupt(); + } + } + } + else { + logger.info("Shutdown ignored - container is already stopped"); + runCallbackIfNotNull(callback); + return; + } + } + finally { + this.consumersLock.unlock(); + } + + Runnable awaitShutdown = () -> { + logger.info("Waiting for workers to finish."); + try { + boolean finished = this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS); + if (finished) { + logger.info("Successfully waited for workers to finish."); } else { - logger.info("Shutdown ignored - container is already stopped"); - return; + logger.info("Workers not finished."); + if (isForceCloseChannel() || this.stopNow.get()) { + canceledConsumers.forEach(consumer -> { + if (logger.isWarnEnabled()) { + logger.warn("Closing channel for unresponsive consumer: " + consumer); + } + consumer.stop(); + }); + } } } - logger.info("Waiting for workers to finish."); - boolean finished = this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS); - if (finished) { - logger.info("Successfully waited for workers to finish."); + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted waiting for workers. Continuing with shutdown."); } - else { - logger.info("Workers not finished."); - if (isForceCloseChannel()) { - canceledConsumers.forEach(consumer -> { - if (logger.isWarnEnabled()) { - logger.warn("Closing channel for unresponsive consumer: " + consumer); - } - consumer.stop(); - }); - } + + this.consumersLock.lock(); + try { + this.consumers = null; + this.cancellationLock.deactivate(); + } + finally { + this.consumersLock.unlock(); } + this.stopNow.set(false); + runCallbackIfNotNull(callback); + }; + if (callback == null) { + awaitShutdown.run(); } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.warn("Interrupted waiting for workers. Continuing with shutdown."); + else { + getTaskExecutor().execute(awaitShutdown); } + } - synchronized (this.consumersMonitor) { - this.consumers = null; - this.cancellationLock.deactivate(); + private void runCallbackIfNotNull(@Nullable Runnable callback) { + if (callback != null) { + callback.run(); } - } private boolean isActive(BlockingQueueConsumer consumer) { boolean consumerActive; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { consumerActive = this.consumers != null && this.consumers.contains(consumer); } + finally { + this.consumersLock.unlock(); + } return consumerActive && this.isActive(); } protected int initializeConsumers() { int count = 0; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers == null) { this.cancellationLock.reset(); - this.consumers = new HashSet(this.concurrentConsumers); + this.consumers = new HashSet<>(this.concurrentConsumers); for (int i = 1; i <= this.concurrentConsumers; i++) { BlockingQueueConsumer consumer = createBlockingQueueConsumer(); if (getConsumeDelay() > 0) { @@ -685,6 +767,9 @@ protected int initializeConsumers() { } } } + finally { + this.consumersLock.unlock(); + } return count; } @@ -695,13 +780,18 @@ protected int initializeConsumers() { */ protected void adjustConsumers(int deltaArg) { int delta = deltaArg; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (isActive() && this.consumers != null) { if (delta > 0) { Iterator consumerIterator = this.consumers.iterator(); - while (consumerIterator.hasNext() && delta > 0 - && (this.maxConcurrentConsumers == null - || this.consumers.size() > this.maxConcurrentConsumers)) { + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; + while (consumerIterator.hasNext() && delta > 0) { + if (!(maxConcurrentConsumersToCheck == null + || this.consumers.size() > maxConcurrentConsumersToCheck)) { + + break; + } BlockingQueueConsumer consumer = consumerIterator.next(); consumer.basicCancel(true); consumerIterator.remove(); @@ -713,19 +803,23 @@ protected void adjustConsumers(int deltaArg) { } } } + finally { + this.consumersLock.unlock(); + } } - /** * Start up to delta consumers, limited by {@link #setMaxConcurrentConsumers(int)}. * @param delta the consumers to add. */ protected void addAndStartConsumers(int delta) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null) { + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; for (int i = 0; i < delta; i++) { - if (this.maxConcurrentConsumers != null - && this.consumers.size() >= this.maxConcurrentConsumers) { + if (maxConcurrentConsumersToCheck != null + && this.consumers.size() >= maxConcurrentConsumersToCheck) { break; } BlockingQueueConsumer consumer = createBlockingQueueConsumer(); @@ -757,12 +851,18 @@ protected void addAndStartConsumers(int delta) { } } } + finally { + this.consumersLock.unlock(); + } } private void considerAddingAConsumer() { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; if (this.consumers != null - && this.maxConcurrentConsumers != null && this.consumers.size() < this.maxConcurrentConsumers) { + && maxConcurrentConsumersToCheck != null && this.consumers.size() < maxConcurrentConsumersToCheck) { + long now = System.currentTimeMillis(); if (this.lastConsumerStarted + this.startConsumerMinInterval < now) { this.addAndStartConsumers(1); @@ -770,11 +870,14 @@ private void considerAddingAConsumer() { } } } + finally { + this.consumersLock.unlock(); + } } - private void considerStoppingAConsumer(BlockingQueueConsumer consumer) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null && this.consumers.size() > this.concurrentConsumers) { long now = System.currentTimeMillis(); if (this.lastConsumerStopped + this.stopConsumerMinInterval < now) { @@ -787,10 +890,14 @@ private void considerStoppingAConsumer(BlockingQueueConsumer consumer) { } } } + finally { + this.consumersLock.unlock(); + } } private void queuesChanged() { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null) { int count = 0; Iterator consumerIterator = this.consumers.iterator(); @@ -812,6 +919,9 @@ private void queuesChanged() { addAndStartConsumers(count); } } + finally { + this.consumersLock.unlock(); + } } protected BlockingQueueConsumer createBlockingQueueConsumer() { @@ -819,7 +929,7 @@ protected BlockingQueueConsumer createBlockingQueueConsumer() { String[] queues = getQueueNames(); // There's no point prefetching less than the tx size, otherwise the consumer will stall because the broker // didn't get an ack for delivered messages - int actualPrefetchCount = getPrefetchCount() > this.batchSize ? getPrefetchCount() : this.batchSize; + int actualPrefetchCount = Math.max(getPrefetchCount(), this.batchSize); consumer = new BlockingQueueConsumer(getConnectionFactory(), getMessagePropertiesConverter(), this.cancellationLock, getAcknowledgeMode(), isChannelTransacted(), actualPrefetchCount, isDefaultRequeueRejected(), getConsumerArguments(), isNoLocal(), isExclusive(), queues); @@ -841,13 +951,16 @@ this.cancellationLock, getAcknowledgeMode(), isChannelTransacted(), actualPrefet consumer.setBackOffExecution(getRecoveryBackOff().start()); consumer.setShutdownTimeout(getShutdownTimeout()); consumer.setApplicationEventPublisher(getApplicationEventPublisher()); + consumer.setMessageAckListener(getMessageAckListener()); return consumer; } private void restart(BlockingQueueConsumer oldConsumer) { BlockingQueueConsumer consumer = oldConsumer; - synchronized (this.consumersMonitor) { - if (this.consumers != null) { + this.consumersLock.lock(); + try { + Set consumersToUse = this.consumers; + if (consumersToUse != null) { try { // Need to recycle the channel in this consumer consumer.stop(); @@ -855,7 +968,7 @@ private void restart(BlockingQueueConsumer oldConsumer) { // to start because of the exception, but // we haven't counted down yet) this.cancellationLock.release(consumer); - this.consumers.remove(consumer); + consumersToUse.remove(consumer); if (!isActive()) { // Do not restart - container is stopping return; @@ -863,7 +976,7 @@ private void restart(BlockingQueueConsumer oldConsumer) { BlockingQueueConsumer newConsumer = createBlockingQueueConsumer(); newConsumer.setBackOffExecution(consumer.getBackOffExecution()); consumer = newConsumer; - this.consumers.add(consumer); + consumersToUse.add(consumer); if (getApplicationEventPublisher() != null) { getApplicationEventPublisher() .publishEvent(new AsyncConsumerRestartedEvent(this, oldConsumer, newConsumer)); @@ -878,8 +991,12 @@ private void restart(BlockingQueueConsumer oldConsumer) { .execute(new AsyncMessageProcessingConsumer(consumer)); } } + finally { + this.consumersLock.unlock(); + } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private boolean receiveAndExecute(final BlockingQueueConsumer consumer) throws Exception { // NOSONAR PlatformTransactionManager transactionManager = getTransactionManager(); @@ -890,7 +1007,7 @@ private boolean receiveAndExecute(final BlockingQueueConsumer consumer) throws E new TransactionTemplate(transactionManager, getTransactionAttribute()); } return this.transactionTemplate - .execute(status -> { // NOSONAR null never returned + .execute(status -> { RabbitResourceHolder resourceHolder = ConnectionFactoryUtils.bindResourceToTransaction( new RabbitResourceHolder(consumer.getChannel(), false), getConnectionFactory(), true); @@ -910,6 +1027,9 @@ private boolean receiveAndExecute(final BlockingQueueConsumer consumer) throws E catch (WrappedTransactionException e) { // NOSONAR exception flow control throw (Exception) e.getCause(); } + finally { + ConnectionFactoryUtils.checkAfterCompletion(); + } } return doReceiveAndExecute(consumer); @@ -922,42 +1042,43 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep List messages = null; long deliveryTag = 0; - + boolean immediateAck = false; + boolean isBatchReceiveTimeoutEnabled = this.batchReceiveTimeout > 0; + long startTime = isBatchReceiveTimeoutEnabled ? System.currentTimeMillis() : 0; for (int i = 0; i < this.batchSize; i++) { + boolean batchTimedOut = isBatchReceiveTimeoutEnabled && + (System.currentTimeMillis() - startTime) > this.batchReceiveTimeout; + if (batchTimedOut) { + if (logger.isTraceEnabled()) { + long gathered = messages != null ? messages.size() : 0; + logger.trace("Timed out for gathering batch messages. gathered size is " + gathered); + } + break; + } logger.trace("Waiting for message from consumer."); Message message = consumer.nextMessage(this.receiveTimeout); if (message == null) { break; } + MessageProperties messageProperties = message.getMessageProperties(); if (this.consumerBatchEnabled) { Collection afterReceivePostProcessors = getAfterReceivePostProcessors(); if (afterReceivePostProcessors != null) { - Message original = message; - deliveryTag = message.getMessageProperties().getDeliveryTag(); - for (MessagePostProcessor processor : getAfterReceivePostProcessors()) { + deliveryTag = messageProperties.getDeliveryTag(); + for (MessagePostProcessor processor : afterReceivePostProcessors) { message = processor.postProcessMessage(message); - if (message == null) { - channel.basicAck(deliveryTag, false); - if (this.logger.isDebugEnabled()) { - this.logger.debug( - "Message Post Processor returned 'null', discarding message " + original); - } - break; - } } } - if (message != null) { - if (messages == null) { - messages = new ArrayList<>(this.batchSize); - } - if (isDeBatchingEnabled() && getBatchingStrategy().canDebatch(message.getMessageProperties())) { - final List messageList = messages; - getBatchingStrategy().deBatch(message, fragment -> messageList.add(fragment)); - } - else { - messages.add(message); - } + if (messages == null) { + messages = new ArrayList<>(this.batchSize); + } + BatchingStrategy batchingStrategy = getBatchingStrategy(); + if (isDeBatchingEnabled() && batchingStrategy.canDebatch(messageProperties)) { + batchingStrategy.deBatch(message, messages::add); + } + else { + messages.add(message); } } else { @@ -972,22 +1093,28 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep if (this.logger.isDebugEnabled()) { this.logger.debug("User requested ack for failed delivery '" + e.getMessage() + "': " - + message.getMessageProperties().getDeliveryTag()); + + messageProperties.getDeliveryTag()); } + immediateAck = this.enforceImmediateAckForManual; break; } catch (Exception ex) { if (causeChainHasImmediateAcknowledgeAmqpException(ex)) { if (this.logger.isDebugEnabled()) { this.logger.debug("User requested ack for failed delivery: " - + message.getMessageProperties().getDeliveryTag()); + + messageProperties.getDeliveryTag()); } + immediateAck = this.enforceImmediateAckForManual; break; } + long tagToRollback = isAsyncReplies() + ? messageProperties.getDeliveryTag() + : -1; if (getTransactionManager() != null) { if (getTransactionAttribute().rollbackOn(ex)) { - RabbitResourceHolder resourceHolder = (RabbitResourceHolder) TransactionSynchronizationManager - .getResource(getConnectionFactory()); + RabbitResourceHolder resourceHolder = + (RabbitResourceHolder) TransactionSynchronizationManager.getResource( + getConnectionFactory()); if (resourceHolder != null) { consumer.clearDeliveryTags(); } @@ -996,7 +1123,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep * If we don't actually have a transaction, we have to roll back * manually. See prepareHolderForRollback(). */ - consumer.rollbackOnExceptionIfNecessary(ex); + consumer.rollbackOnExceptionIfNecessary(ex, tagToRollback); } throw ex; // encompassing transaction will handle the rollback. } @@ -1008,21 +1135,21 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep } } else { - consumer.rollbackOnExceptionIfNecessary(ex); + consumer.rollbackOnExceptionIfNecessary(ex, tagToRollback); throw ex; } } } } if (messages != null) { - executeWithList(channel, messages, deliveryTag, consumer); + immediateAck = executeWithList(channel, messages, deliveryTag, consumer); } - return consumer.commitIfNecessary(isChannelLocallyTransacted()); + return consumer.commitIfNecessary(isChannelLocallyTransacted(), immediateAck); } - private void executeWithList(Channel channel, List messages, long deliveryTag, + private boolean executeWithList(Channel channel, List messages, long deliveryTag, BlockingQueueConsumer consumer) { try { @@ -1034,7 +1161,7 @@ private void executeWithList(Channel channel, List messages, long deliv + e.getMessage() + "' (last in batch): " + deliveryTag); } - return; + return this.enforceImmediateAckForManual; } catch (Exception ex) { if (causeChainHasImmediateAcknowledgeAmqpException(ex)) { @@ -1042,7 +1169,7 @@ private void executeWithList(Channel channel, List messages, long deliv this.logger.debug("User requested ack for failed delivery (last in batch): " + deliveryTag); } - return; + return this.enforceImmediateAckForManual; } if (getTransactionManager() != null) { if (getTransactionAttribute().rollbackOn(ex)) { @@ -1071,17 +1198,22 @@ private void executeWithList(Channel channel, List messages, long deliv throw ex; } } + return false; } protected void handleStartupFailure(BackOffExecution backOffExecution) { long recoveryInterval = backOffExecution.nextBackOff(); if (BackOffExecution.STOP == recoveryInterval) { - synchronized (this) { + this.lifecycleLock.lock(); + try { if (isActive()) { logger.warn("stopping container - restart recovery attempts exhausted"); stop(); } } + finally { + this.lifecycleLock.unlock(); + } return; } try { @@ -1100,7 +1232,7 @@ protected void handleStartupFailure(BackOffExecution backOffExecution) { } @Override - protected void publishConsumerFailedEvent(String reason, boolean fatal, @Nullable Throwable t) { + protected void publishConsumerFailedEvent(@Nullable String reason, boolean fatal, @Nullable Throwable t) { if (!fatal || !isRunning()) { super.publishConsumerFailedEvent(reason, fatal, t); } @@ -1117,7 +1249,7 @@ protected void publishConsumerFailedEvent(String reason, boolean fatal, @Nullabl @Override public String toString() { return "SimpleMessageListenerContainer " - + (getBeanName() != null ? "(" + getBeanName() + ") " : "") + + "(" + getBeanName() + ") " + "[concurrentConsumers=" + this.concurrentConsumers + (this.maxConcurrentConsumers != null ? ", maxConcurrentConsumers=" + this.maxConcurrentConsumers : "") + ", queueNames=" + Arrays.toString(getQueueNames()) + "]"; @@ -1131,12 +1263,13 @@ private final class AsyncMessageProcessingConsumer implements Runnable { private final CountDownLatch start; - private volatile FatalListenerStartupException startupException; + private volatile @Nullable FatalListenerStartupException startupException; private int consecutiveIdles; private int consecutiveMessages; + private boolean failedExclusive; AsyncMessageProcessingConsumer(BlockingQueueConsumer consumer) { this.consumer = consumer; @@ -1151,7 +1284,7 @@ private final class AsyncMessageProcessingConsumer implements Runnable { * @return a startup exception if there was one * @throws InterruptedException if the consumer startup is interrupted */ - private FatalListenerStartupException getStartupException() throws InterruptedException { + private @Nullable FatalListenerStartupException getStartupException() throws InterruptedException { if (!this.start.await( SimpleMessageListenerContainer.this.consumerStartTimeout, TimeUnit.MILLISECONDS)) { logger.error("Consumer failed to start in " @@ -1165,6 +1298,7 @@ private FatalListenerStartupException getStartupException() throws InterruptedEx @Override // NOSONAR - complexity - many catch blocks public void run() { // NOSONAR - line count if (!isActive()) { + this.start.countDown(); return; } @@ -1173,8 +1307,10 @@ public void run() { // NOSONAR - line count this.consumer.setLocallyTransacted(isChannelLocallyTransacted()); String routingLookupKey = getRoutingLookupKey(); + RoutingConnectionFactory routingConnectionFactoryToUse = getRoutingConnectionFactory(); if (routingLookupKey != null) { - SimpleResourceHolder.bind(getRoutingConnectionFactory(), routingLookupKey); // NOSONAR both never null + Assert.state(routingConnectionFactoryToUse != null, "'routingConnectionFactory' must be provided."); + SimpleResourceHolder.bind(routingConnectionFactoryToUse, routingLookupKey); // NOSONAR both never null } if (this.consumer.getQueueCount() < 1) { @@ -1247,10 +1383,16 @@ public void run() { // NOSONAR - line count } } catch (AmqpIOException e) { - if (e.getCause() instanceof IOException && e.getCause().getCause() instanceof ShutdownSignalException - && e.getCause().getCause().getMessage().contains("in exclusive use")) { - getExclusiveConsumerExceptionLogger().log(logger, - "Exclusive consumer failure", e.getCause().getCause()); + if (RabbitUtils.exclusiveAccesssRefused(e)) { + this.failedExclusive = true; + Throwable cause = e.getCause(); + if (cause != null) { + cause = cause.getCause() == null ? cause : cause.getCause(); + } + else { + cause = e; + } + getExclusiveConsumerExceptionLogger().log(logger, "Exclusive consumer failure", cause); publishConsumerFailedEvent("Consumer raised exception, attempting restart", false, e); } else { @@ -1270,6 +1412,7 @@ public void run() { // NOSONAR - line count } } finally { + SimpleMessageListenerContainer.this.cancellationLock.release(this.consumer); if (getTransactionManager() != null) { ConsumerChannelRegistry.unRegisterConsumerChannel(); } @@ -1280,13 +1423,17 @@ public void run() { // NOSONAR - line count killOrRestart(aborted); - if (routingLookupKey != null) { - SimpleResourceHolder.unbind(getRoutingConnectionFactory()); // NOSONAR never null here + if (routingConnectionFactoryToUse != null) { + SimpleResourceHolder.unbind(routingConnectionFactoryToUse); } } private void mainLoop() throws Exception { // NOSONAR Exception try { + if (SimpleMessageListenerContainer.this.stopNow.get()) { + this.consumer.forceCloseAndClearQueue(); + return; + } boolean receivedOk = receiveAndExecute(this.consumer); // At least one message received if (SimpleMessageListenerContainer.this.maxConcurrentConsumers != null) { checkAdjust(receivedOk); @@ -1364,7 +1511,7 @@ private void initialize() throws Throwable { // NOSONAR throw ex; } else { - Throwable possibleAuthException = ex.getCause().getCause(); + Throwable possibleAuthException = findAuthException(ex); if (!(possibleAuthException instanceof PossibleAuthenticationFailureException)) { throw ex; } @@ -1390,14 +1537,26 @@ private void initialize() throws Throwable { // NOSONAR } } + private static Throwable findAuthException(Throwable ex) { + Throwable cause = ex.getCause(); + if (cause != null) { + cause = cause.getCause(); + if (cause != null) { + return cause; + } + } + return ex; + } + private void killOrRestart(boolean aborted) { if (!isActive(this.consumer) || aborted) { logger.debug("Cancelling " + this.consumer); try { this.consumer.stop(); SimpleMessageListenerContainer.this.cancellationLock.release(this.consumer); - if (getApplicationEventPublisher() != null) { - getApplicationEventPublisher().publishEvent( + ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); + if (applicationEventPublisher != null && !isApplicationContextClosed()) { + applicationEventPublisher.publishEvent( new AsyncConsumerStoppedEvent(SimpleMessageListenerContainer.this, this.consumer)); } } @@ -1427,7 +1586,13 @@ private void killOrRestart(boolean aborted) { } } else { - logger.info("Restarting " + this.consumer); + LogMessage restartMessage = LogMessage.of(() -> "Restarting " + this.consumer); + if (this.failedExclusive) { + getExclusiveConsumerExceptionLogger().logRestart(logger, restartMessage); + } + else { + logger.info(restartMessage); + } restart(this.consumer); } } @@ -1435,7 +1600,9 @@ private void killOrRestart(boolean aborted) { private void logConsumerException(Throwable t) { if (logger.isDebugEnabled() || !(t instanceof AmqpConnectException || t instanceof ConsumerCancelledException)) { - logger.debug( + // It has to be WARN independently of condition. + // The meaning is: log WARN for all exception when DEBUG enabled, or all others, but mentioned + logger.warn( "Consumer raised exception, processing can restart if the connection factory supports it", t); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index 03b4e84159..88e395d67c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author 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,12 @@ import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.Arrays; -import java.util.function.Consumer; +import java.util.concurrent.CompletableFuture; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AcknowledgeMode; @@ -53,10 +55,6 @@ import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.concurrent.ListenableFuture; - -import com.rabbitmq.client.Channel; -import reactor.core.publisher.Mono; /** * An abstract {@link org.springframework.amqp.core.MessageListener} adapter providing the @@ -81,7 +79,7 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe private static final ParserContext PARSER_CONTEXT = new TemplateParserContext("!{", "}"); - private static final boolean monoPresent = // NOSONAR - lower case + static final boolean monoPresent = // NOSONAR - lower case, protected ClassUtils.isPresent("reactor.core.publisher.Mono", ChannelAwareMessageListener.class.getClassLoader()); /** @@ -91,35 +89,35 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe private final StandardEvaluationContext evalContext = new StandardEvaluationContext(); + private final MessagePropertiesConverter messagePropertiesConverter = new DefaultMessagePropertiesConverter(); + private String responseRoutingKey = DEFAULT_RESPONSE_ROUTING_KEY; - private String responseExchange = null; + private @Nullable String responseExchange; - private Address responseAddress = null; + private @Nullable Address responseAddress; - private Expression responseExpression; + private @Nullable Expression responseExpression; private boolean mandatoryPublish; - private MessageConverter messageConverter = new SimpleMessageConverter(); - - private volatile MessagePropertiesConverter messagePropertiesConverter = new DefaultMessagePropertiesConverter(); + private @Nullable MessageConverter messageConverter = new SimpleMessageConverter(); private String encoding = DEFAULT_ENCODING; - private MessagePostProcessor[] beforeSendReplyPostProcessors; + private MessagePostProcessor @Nullable [] beforeSendReplyPostProcessors; - private RetryTemplate retryTemplate; + private @Nullable RetryTemplate retryTemplate; - private RecoveryCallback recoveryCallback; + private @Nullable RecoveryCallback recoveryCallback; private boolean isManualAck; private boolean defaultRequeueRejected = true; - private ReplyPostProcessor replyPostProcessor; + private @Nullable ReplyPostProcessor replyPostProcessor; - private String replyContentType; + private @Nullable String replyContentType; private boolean converterWinsContentType = true; @@ -203,7 +201,7 @@ public void setMandatoryPublish(boolean mandatoryPublish) { * The default converter is a {@link SimpleMessageConverter}, which is able to handle "text" content-types. * @param messageConverter The message converter. */ - public void setMessageConverter(MessageConverter messageConverter) { + public void setMessageConverter(@Nullable MessageConverter messageConverter) { this.messageConverter = messageConverter; } @@ -218,6 +216,10 @@ public void setBeforeSendReplyPostProcessors(MessagePostProcessor... beforeSendR beforeSendReplyPostProcessors.length); } + public MessagePostProcessor @Nullable [] getBeforeSendReplyPostProcessors() { + return this.beforeSendReplyPostProcessors; + } + /** * Set a {@link RetryTemplate} to use when sending replies. * @param retryTemplate the template. @@ -244,8 +246,10 @@ public void setRecoveryCallback(RecoveryCallback recoveryCallback) { * @param beanResolver the resolver. * @since 1.6 */ - public void setBeanResolver(BeanResolver beanResolver) { - this.evalContext.setBeanResolver(beanResolver); + public void setBeanResolver(@Nullable BeanResolver beanResolver) { + if (beanResolver != null) { + this.evalContext.setBeanResolver(beanResolver); + } this.evalContext.setTypeConverter(new StandardTypeConverter()); this.evalContext.addPropertyAccessor(new MapAccessor()); } @@ -266,7 +270,7 @@ public void setReplyPostProcessor(ReplyPostProcessor replyPostProcessor) { * @return the content type. * @since 2.3 */ - protected String getReplyContentType() { + protected @Nullable String getReplyContentType() { return this.replyContentType; } @@ -302,7 +306,7 @@ public void setConverterWinsContentType(boolean converterWinsContentType) { * returned from listener methods back to Rabbit messages. * @return The message converter. */ - protected MessageConverter getMessageConverter() { + protected @Nullable MessageConverter getMessageConverter() { return this.messageConverter; } @@ -316,11 +320,19 @@ public void setDefaultRequeueRejected(boolean defaultRequeueRejected) { this.defaultRequeueRejected = defaultRequeueRejected; } + protected boolean isDefaultRequeueRejected() { + return this.defaultRequeueRejected; + } + @Override public void containerAckMode(AcknowledgeMode mode) { this.isManualAck = AcknowledgeMode.MANUAL.equals(mode); } + protected boolean isManualAck() { + return this.isManualAck; + } + /** * Handle the given exception that arose during listener execution. * The default implementation logs the exception at error level. @@ -340,10 +352,7 @@ protected void handleListenerException(Throwable ex) { */ protected Object extractMessage(Message message) { MessageConverter converter = getMessageConverter(); - if (converter != null) { - return converter.fromMessage(message); - } - return message; + return converter != null ? converter.fromMessage(message) : message; } /** @@ -351,22 +360,22 @@ protected Object extractMessage(Message message) { * response message back. * @param resultArg the result object to handle (never null) * @param request the original request message - * @param channel the Rabbit channel to operate on (may be null) + * @param channel the Rabbit channel to operate on (maybe null) * @see #buildMessage * @see #postProcessResponse * @see #getReplyToAddress(Message, Object, InvocationResult) * @see #sendResponse */ - protected void handleResult(InvocationResult resultArg, Message request, Channel channel) { + protected void handleResult(InvocationResult resultArg, Message request, @Nullable Channel channel) { handleResult(resultArg, request, channel, null); } /** * Handle the given result object returned from the listener method, sending a * response message back. - * @param resultArg the result object to handle (never null) + * @param resultArg the result object to handle * @param request the original request message - * @param channel the Rabbit channel to operate on (may be null) + * @param channel the Rabbit channel to operate on (maybe null) * @param source the source data for the method invocation - e.g. * {@code o.s.messaging.Message}; may be null * @see #buildMessage @@ -374,52 +383,53 @@ protected void handleResult(InvocationResult resultArg, Message request, Channel * @see #getReplyToAddress(Message, Object, InvocationResult) * @see #sendResponse */ - protected void handleResult(InvocationResult resultArg, Message request, Channel channel, Object source) { - if (channel != null) { - if (resultArg.getReturnValue() instanceof ListenableFuture) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected void handleResult(@Nullable InvocationResult resultArg, Message request, + @Nullable Channel channel, @Nullable Object source) { + + if (resultArg != null) { + if (resultArg.getReturnValue() instanceof CompletableFuture completable) { if (!this.isManualAck) { this.logger.warn("Container AcknowledgeMode must be MANUAL for a Future return type; " + "otherwise the container will ack the message immediately"); } - ((ListenableFuture) resultArg.getReturnValue()).addCallback( - r -> { - asyncSuccess(resultArg, request, channel, source, r); - basicAck(request, channel); - }, - t -> asyncFailure(request, channel, t)); + completable.whenComplete((r, t) -> { + if (t == null) { + asyncSuccess(resultArg, request, channel, source, r); + basicAck(request, channel); + } + else { + asyncFailure(request, channel, t, source); + } + }); } else if (monoPresent && MonoHandler.isMono(resultArg.getReturnValue())) { if (!this.isManualAck) { - this.logger.warn("Container AcknowledgeMode must be MANUAL for a Mono return type; " - + "otherwise the container will ack the message immediately"); + this.logger.warn("Container AcknowledgeMode must be MANUAL for a Mono return type" + + "(or Kotlin suspend function); otherwise the container will ack the message immediately"); } MonoHandler.subscribe(resultArg.getReturnValue(), r -> asyncSuccess(resultArg, request, channel, source, r), - t -> asyncFailure(request, channel, t), + t -> asyncFailure(request, channel, t, source), () -> basicAck(request, channel)); } else { doHandleResult(resultArg, request, channel, source); } } - else if (this.logger.isWarnEnabled()) { - this.logger.warn("Listener method returned result [" + resultArg - + "]: not generating response message for it because no Rabbit Channel given"); - } } - private void asyncSuccess(InvocationResult resultArg, Message request, Channel channel, Object source, - Object deferredResult) { + private void asyncSuccess(InvocationResult resultArg, Message request, @Nullable Channel channel, + @Nullable Object source, @Nullable Object deferredResult) { if (deferredResult == null) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Async result is null, ignoring"); - } + this.logger.debug("Async result is null, ignoring"); } else { - // We only get here with Mono and ListenableFuture which have exactly one type argument Type returnType = resultArg.getReturnType(); - if (returnType != null) { + // We only get here with Mono and CompletableFuture which have exactly one type argument + // Otherwise it might be Kotlin suspend function + if (returnType != null && !Object.class.getName().equals(returnType.getTypeName())) { Type[] actualTypeArguments = ((ParameterizedType) returnType).getActualTypeArguments(); if (actualTypeArguments.length > 0) { returnType = actualTypeArguments[0]; // NOSONAR @@ -437,17 +447,20 @@ private void asyncSuccess(InvocationResult resultArg, Message request, Channel c } } - private void basicAck(Message request, Channel channel) { - try { - channel.basicAck(request.getMessageProperties().getDeliveryTag(), false); - } - catch (IOException e) { - this.logger.error("Failed to ack message", e); + protected void basicAck(Message request, @Nullable Channel channel) { + if (channel != null) { + try { + channel.basicAck(request.getMessageProperties().getDeliveryTag(), false); + } + catch (IOException e) { + this.logger.error("Failed to ack message", e); + } } } - private void asyncFailure(Message request, Channel channel, Throwable t) { - this.logger.error("Future or Mono was completed with an exception for " + request, t); + protected void asyncFailure(Message request, @Nullable Channel channel, Throwable t, @Nullable Object source) { + this.logger.error("Future, Mono, or suspend function was completed with an exception for " + request, t); + Assert.notNull(channel, "'channel' must not be null."); try { channel.basicNack(request.getMessageProperties().getDeliveryTag(), false, ContainerUtils.shouldRequeue(this.defaultRequeueRejected, t, this.logger)); @@ -457,7 +470,9 @@ private void asyncFailure(Message request, Channel channel, Throwable t) { } } - protected void doHandleResult(InvocationResult resultArg, Message request, Channel channel, Object source) { + protected void doHandleResult(InvocationResult resultArg, Message request, @Nullable Channel channel, + @Nullable Object source) { + if (this.logger.isDebugEnabled()) { this.logger.debug("Listener method returned result [" + resultArg + "] - generating response message for it"); @@ -479,29 +494,32 @@ protected void doHandleResult(InvocationResult resultArg, Message request, Chann } } - protected String getReceivedExchange(Message request) { + protected @Nullable String getReceivedExchange(Message request) { return request.getMessageProperties().getReceivedExchange(); } /** * Build a Rabbit message to be sent as response based on the given result object. * @param channel the Rabbit Channel to operate on. + * Can be null if implementation does not support AMQP 0.9.1. * @param result the content of the message, as returned from the listener method. * @param genericType the generic type to populate type headers. * @return the Rabbit Message (never null). * @see #setMessageConverter */ - protected Message buildMessage(Channel channel, Object result, Type genericType) { + protected Message buildMessage(@Nullable Channel channel, @Nullable Object result, @Nullable Type genericType) { MessageConverter converter = getMessageConverter(); if (converter != null && !(result instanceof Message)) { return convert(result, genericType, converter); } else { - if (!(result instanceof Message)) { + if (result instanceof Message msg) { + return msg; + } + else { throw new MessageConversionException("No MessageConverter specified - cannot handle message [" + result + "]"); } - return (Message) result; } } @@ -513,12 +531,18 @@ protected Message buildMessage(Channel channel, Object result, Type genericType) * @return the message. * @since 2.3 */ - protected Message convert(Object result, Type genericType, MessageConverter converter) { + protected Message convert(@Nullable Object result, @Nullable Type genericType, MessageConverter converter) { MessageProperties messageProperties = new MessageProperties(); if (this.replyContentType != null) { messageProperties.setContentType(this.replyContentType); } - Message message = converter.toMessage(result, messageProperties, genericType); + Message message; + if (result == null) { + message = new Message(new byte[0], messageProperties); + } + else { + message = converter.toMessage(result, messageProperties, genericType); + } if (this.replyContentType != null && !this.converterWinsContentType) { message.getMessageProperties().setContentType(this.replyContentType); } @@ -562,7 +586,7 @@ protected void postProcessResponse(Message request, Message response) { * @see org.springframework.amqp.core.Message#getMessageProperties() * @see org.springframework.amqp.core.MessageProperties#getReplyTo() */ - protected Address getReplyToAddress(Message request, Object source, InvocationResult result) { + protected Address getReplyToAddress(Message request, @Nullable Object source, InvocationResult result) { Address replyTo = request.getMessageProperties().getReplyToAddress(); if (replyTo == null) { if (this.responseAddress == null && this.responseExchange != null) { @@ -587,13 +611,15 @@ else if (this.responseAddress == null) { return replyTo; } - private Address evaluateReplyTo(Message request, Object source, Object result, Expression expression) { + private Address evaluateReplyTo(Message request, @Nullable Object source, @Nullable Object result, + Expression expression) { + Address replyTo; Object value = expression.getValue(this.evalContext, new ReplyExpressionRoot(request, source, result)); Assert.state(value instanceof String || value instanceof Address, "response expression must evaluate to a String or Address"); - if (value instanceof String) { - replyTo = new Address((String) value); + if (value instanceof String sValue) { + replyTo = new Address(sValue); } else { replyTo = (Address) value; @@ -609,7 +635,8 @@ private Address evaluateReplyTo(Message request, Object source, Object result, E * @see #postProcessResponse(Message, Message) * @see #setReplyPostProcessor(ReplyPostProcessor) */ - protected void sendResponse(Channel channel, Address replyTo, Message messageIn) { + protected void sendResponse(@Nullable Channel channel, Address replyTo, Message messageIn) { + Assert.notNull(channel, "'channel' must not be null."); Message message = messageIn; if (this.beforeSendReplyPostProcessors != null) { for (MessagePostProcessor postProcessor : this.beforeSendReplyPostProcessors) { @@ -657,7 +684,6 @@ protected void doPublish(Channel channel, Address replyTo, Message message) thro * Post-process the given message before sending the response. *

* The default implementation is empty. - * * @param channel The channel. * @param response the outgoing Rabbit message about to be sent */ @@ -671,11 +697,11 @@ public static final class ReplyExpressionRoot { private final Message request; - private final Object source; + private final @Nullable Object source; - private final Object result; + private final @Nullable Object result; - protected ReplyExpressionRoot(Message request, Object source, Object result) { + protected ReplyExpressionRoot(Message request, @Nullable Object source, @Nullable Object result) { this.request = request; this.source = source; this.result = result; @@ -685,29 +711,14 @@ public Message getRequest() { return this.request; } - public Object getSource() { + public @Nullable Object getSource() { return this.source; } - public Object getResult() { + public @Nullable Object getResult() { return this.result; } } - private static class MonoHandler { // NOSONAR - pointless to name it ..Utils|Helper - - static boolean isMono(Object result) { - return result instanceof Mono; - } - - @SuppressWarnings("unchecked") - static void subscribe(Object returnValue, Consumer success, - Consumer failure, Runnable completeConsumer) { - - ((Mono) returnValue).subscribe(success, failure, completeConsumer); - } - - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java new file mode 100644 index 0000000000..d73e4e8567 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java @@ -0,0 +1,145 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener.adapter; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.KotlinDetector; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.converter.MessageConverter; +import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; +import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; +import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.validation.Validator; + +/** + * Extension of the {@link DefaultMessageHandlerMethodFactory} for Spring AMQP requirements. + * + * @author Artem Bilan + * + * @since 3.0.5 + */ +public class AmqpMessageHandlerMethodFactory extends DefaultMessageHandlerMethodFactory { + + private final HandlerMethodArgumentResolverComposite argumentResolvers = + new HandlerMethodArgumentResolverComposite(); + + @SuppressWarnings("NullAway.Init") + private MessageConverter messageConverter; + + private @Nullable Validator validator; + + @Override + public void setMessageConverter(MessageConverter messageConverter) { + super.setMessageConverter(messageConverter); + this.messageConverter = messageConverter; + } + + @Override + public void setValidator(Validator validator) { + super.setValidator(validator); + this.validator = validator; + } + + @Override + protected List initArgumentResolvers() { + List resolvers = super.initArgumentResolvers(); + if (KotlinDetector.isKotlinPresent()) { + // Insert before PayloadMethodArgumentResolver + resolvers.add(resolvers.size() - 1, new ContinuationHandlerMethodArgumentResolver()); + } + // Has to be at the end, but before PayloadMethodArgumentResolver + resolvers.add(resolvers.size() - 1, + new OptionalEmptyAwarePayloadArgumentResolver(this.messageConverter, this.validator)); + this.argumentResolvers.addResolvers(resolvers); + return resolvers; + } + + @Override + public InvocableHandlerMethod createInvocableHandlerMethod(Object bean, Method method) { + InvocableHandlerMethod handlerMethod = new KotlinAwareInvocableHandlerMethod(bean, method); + handlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers); + return handlerMethod; + } + + private static class OptionalEmptyAwarePayloadArgumentResolver extends PayloadMethodArgumentResolver { + + OptionalEmptyAwarePayloadArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator) { + super(messageConverter, validator); + } + + @Override + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { // NOSONAR + Object resolved; + try { + resolved = super.resolveArgument(parameter, message); + } + catch (MethodArgumentNotValidException ex) { + Type type = parameter.getGenericParameterType(); + if (isOptional(message, type)) { + BindingResult bindingResult = ex.getBindingResult(); + if (bindingResult != null) { + List allErrors = bindingResult.getAllErrors(); + if (allErrors.size() == 1) { + String defaultMessage = allErrors.get(0).getDefaultMessage(); + if ("Payload value must not be empty".equals(defaultMessage)) { + return Optional.empty(); + } + } + } + } + throw ex; + } + /* + * Replace Optional.empty() list elements with null. + */ + if (resolved instanceof List list) { + for (int i = 0; i < list.size(); i++) { + if (list.get(i).equals(Optional.empty())) { + list.set(i, null); + } + } + } + return resolved; + } + + private boolean isOptional(Message message, Type type) { + return (Optional.class.equals(type) || + (type instanceof ParameterizedType pType && Optional.class.equals(pType.getRawType()))) + && message.getPayload().equals(Optional.empty()); + } + + @Override + protected boolean isEmptyPayload(@Nullable Object payload) { + return payload == null || payload.equals(Optional.empty()); + } + + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java index 6667a7b94f..144d064b53 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,56 +16,75 @@ package org.springframework.amqp.rabbit.listener.adapter; +import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy; import org.springframework.amqp.rabbit.listener.api.ChannelAwareBatchMessageListener; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; +import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; -import org.springframework.lang.Nullable; +import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.messaging.Message; import org.springframework.messaging.support.GenericMessage; import org.springframework.messaging.support.MessageBuilder; - -import com.rabbitmq.client.Channel; +import org.springframework.util.Assert; /** * A listener adapter for batch listeners. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.2 * */ public class BatchMessagingMessageListenerAdapter extends MessagingMessageListenerAdapter - implements ChannelAwareBatchMessageListener { - - private final MessagingMessageConverterAdapter converterAdapter; + implements ChannelAwareBatchMessageListener { private final BatchingStrategy batchingStrategy; - public BatchMessagingMessageListenerAdapter(Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) { + @SuppressWarnings("this-escape") + public BatchMessagingMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) { super(bean, method, returnExceptions, errorHandler, true); - this.converterAdapter = (MessagingMessageConverterAdapter) getMessagingMessageConverter(); this.batchingStrategy = batchingStrategy == null ? new SimpleBatchingStrategy(0, 0, 0L) : batchingStrategy; } @Override - public void onMessageBatch(List messages, Channel channel) { + public void onMessageBatch(List messages, @Nullable Channel channel) { Message converted; - if (this.converterAdapter.isAmqpMessageList()) { + if (this.messagingMessageConverter.isAmqpMessageList()) { converted = new GenericMessage<>(messages); } else { List> messagingMessages = new ArrayList<>(); for (org.springframework.amqp.core.Message message : messages) { - messagingMessages.add(toMessagingMessage(message)); + try { + Message messagingMessage = toMessagingMessage(message); + messagingMessages.add(messagingMessage); + } + catch (MessageConversionException e) { + this.logger.error("Could not convert incoming message", e); + try { + Assert.notNull(channel, "'channel' cannot be null"); + channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); + } + catch (Exception ex) { + this.logger.error("Failed to reject message with conversion error", ex); + throw e; // NOSONAR + } + } } - if (this.converterAdapter.isMessageList()) { + if (this.messagingMessageConverter.isMessageList()) { converted = new GenericMessage<>(messagingMessages); } else { @@ -77,31 +96,96 @@ public void onMessageBatch(List messages, } } try { - invokeHandlerAndProcessResult(null, channel, converted); + invokeHandlerAndProcessResult(messages, channel, converted); } catch (Exception e) { throw RabbitExceptionTranslator.convertRabbitAccessException(e); } } + private void invokeHandlerAndProcessResult(List amqpMessages, + @Nullable Channel channel, Message message) { + + if (logger.isDebugEnabled()) { + logger.debug("Processing [" + message + "]"); + } + InvocationResult result = invokeHandler(channel, message, true, + amqpMessages.toArray(new org.springframework.amqp.core.Message[0])); + if (result.getReturnValue() != null) { + handleResult(result, amqpMessages, channel); + } + else { + logger.trace("No result object given - no result to handle"); + } + } + + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private void handleResult(InvocationResult resultArg, List amqpMessages, + @Nullable Channel channel) { + + if (channel != null) { + if (resultArg.getReturnValue() instanceof CompletableFuture completable) { + if (!isManualAck()) { + this.logger.warn("Container AcknowledgeMode must be MANUAL for a Future return type; " + + "otherwise the container will ack the message immediately"); + } + completable.whenComplete((r, t) -> { + if (t == null) { + amqpMessages.forEach((request) -> basicAck(request, channel)); + } + else { + asyncFailure(amqpMessages, channel, t); + } + }); + } + else if (monoPresent && MonoHandler.isMono(resultArg.getReturnValue())) { + if (!isManualAck()) { + this.logger.warn("Container AcknowledgeMode must be MANUAL for a Mono return type" + + "(or Kotlin suspend function); otherwise the container will ack the message immediately"); + } + MonoHandler.subscribe(resultArg.getReturnValue(), + null, + t -> asyncFailure(amqpMessages, channel, t), + () -> amqpMessages.forEach((request) -> basicAck(request, channel))); + } + else { + throw new IllegalStateException("The listener in batch mode does not support replies."); + } + } + else if (this.logger.isWarnEnabled()) { + this.logger.warn("Listener method returned result [" + resultArg + + "]: not generating response message for it because no Rabbit Channel given"); + } + } + + private void asyncFailure(List requests, Channel channel, Throwable t) { + this.logger.error("Future, Mono, or suspend function was completed with an exception for " + requests, t); + for (org.springframework.amqp.core.Message request : requests) { + try { + channel.basicNack(request.getMessageProperties().getDeliveryTag(), false, + ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), t, this.logger)); + } + catch (IOException e) { + this.logger.error("Failed to nack message", e); + } + } + } + @Override protected Message toMessagingMessage(org.springframework.amqp.core.Message amqpMessage) { if (this.batchingStrategy.canDebatch(amqpMessage.getMessageProperties())) { - if (this.converterAdapter.isMessageList()) { + if (this.messagingMessageConverter.isMessageList()) { List> messages = new ArrayList<>(); - this.batchingStrategy.deBatch(amqpMessage, fragment -> { - messages.add(super.toMessagingMessage(fragment)); - }); + this.batchingStrategy.deBatch(amqpMessage, fragment -> messages.add(super.toMessagingMessage(fragment))); return new GenericMessage<>(messages); } else { List list = new ArrayList<>(); - this.batchingStrategy.deBatch(amqpMessage, fragment -> { - list.add(this.converterAdapter.extractPayload(fragment)); - }); + this.batchingStrategy.deBatch(amqpMessage, fragment -> + list.add(this.messagingMessageConverter.extractPayload(fragment))); return MessageBuilder.withPayload(list) - .copyHeaders(this.converterAdapter + .copyHeaders(this.messagingMessageConverter .getHeaderMapper() .toHeaders(amqpMessage.getMessageProperties())) .build(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..f35a6dd874 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener.adapter; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; + +/** + * No-op resolver for method arguments of type {@link kotlin.coroutines.Continuation}. + *

+ * This class is similar to + * {@link org.springframework.messaging.handler.annotation.reactive.ContinuationHandlerMethodArgumentResolver} + * but for regular {@link HandlerMethodArgumentResolver} contract. + * + * @author Artem Bilan + * + * @since 3.0.5 + * + * @see org.springframework.messaging.handler.annotation.reactive.ContinuationHandlerMethodArgumentResolver + */ +public class ContinuationHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return "kotlin.coroutines.Continuation".equals(parameter.getParameterType().getName()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) { + return Mono.empty(); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java index 6778848b5a..dd416c0c03 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-2025 the original author 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 java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.beans.factory.config.BeanExpressionContext; @@ -36,7 +38,6 @@ import org.springframework.expression.ParserContext; import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.MessageConverter; @@ -45,9 +46,9 @@ import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; import org.springframework.validation.Validator; - /** * Delegates to an {@link InvocableHandlerMethod} based on the message payload type. * Matches a single, non-annotated parameter or one that is annotated with @@ -73,17 +74,19 @@ public class DelegatingInvocableHandler { private final ConcurrentMap payloadMethodParameters = new ConcurrentHashMap<>(); - private final InvocableHandlerMethod defaultHandler; + private final @Nullable InvocableHandlerMethod defaultHandler; - private final Map handlerSendTo = new HashMap<>(); + private final Map handlerSendTo = new ConcurrentHashMap<>(); private final Object bean; - private final BeanExpressionResolver resolver; + private final @Nullable BeanExpressionResolver resolver; + + private final @Nullable BeanExpressionContext beanExpressionContext; - private final BeanExpressionContext beanExpressionContext; + private final @Nullable PayloadValidator validator; - private final PayloadValidator validator; + private final boolean asyncReplies; /** * Construct an instance with the supplied handlers for the bean. @@ -110,6 +113,7 @@ public DelegatingInvocableHandler(List handlers, Object public DelegatingInvocableHandler(List handlers, @Nullable InvocableHandlerMethod defaultHandler, Object bean, BeanExpressionResolver beanExpressionResolver, BeanExpressionContext beanExpressionContext) { + this(handlers, defaultHandler, bean, beanExpressionResolver, beanExpressionContext, null); } @@ -124,8 +128,9 @@ public DelegatingInvocableHandler(List handlers, * @since 2.0.3 */ public DelegatingInvocableHandler(List handlers, - @Nullable InvocableHandlerMethod defaultHandler, Object bean, BeanExpressionResolver beanExpressionResolver, - BeanExpressionContext beanExpressionContext, @Nullable Validator validator) { + @Nullable InvocableHandlerMethod defaultHandler, Object bean, + @Nullable BeanExpressionResolver beanExpressionResolver, + @Nullable BeanExpressionContext beanExpressionContext, @Nullable Validator validator) { this.handlers = new ArrayList<>(handlers); this.defaultHandler = defaultHandler; @@ -133,9 +138,18 @@ public DelegatingInvocableHandler(List handlers, this.resolver = beanExpressionResolver; this.beanExpressionContext = beanExpressionContext; this.validator = validator == null ? null : new PayloadValidator(validator); + boolean asyncRepl; + asyncRepl = defaultHandler != null && isAsyncReply(defaultHandler); + for (InvocableHandlerMethod handler : handlers) { + asyncRepl |= isAsyncReply(handler); + } + this.asyncReplies = asyncRepl; } - + private boolean isAsyncReply(InvocableHandlerMethod method) { + return (AbstractAdaptableMessageListener.monoPresent && MonoHandler.isMono(method.getMethod().getReturnType())) + || CompletableFuture.class.isAssignableFrom(method.getMethod().getReturnType()); + } /** * @return the bean @@ -144,6 +158,15 @@ public Object getBean() { return this.bean; } + /** + * Return true if any handler method has an async reply type. + * @return the asyncReply. + * @since 2.2.21 + */ + public boolean isAsyncReplies() { + return this.asyncReplies; + } + /** * Invoke the method with the given message. * @param message the message. @@ -152,8 +175,8 @@ public Object getBean() { * @throws Exception raised if no suitable argument resolver can be found, * or the method raised an exception. */ - public InvocationResult invoke(Message message, Object... providedArgs) throws Exception { // NOSONAR - Class payloadClass = message.getPayload().getClass(); + public InvocationResult invoke(Message message, @Nullable Object... providedArgs) throws Exception { // NOSONAR + Class payloadClass = message.getPayload().getClass(); InvocableHandlerMethod handler = getHandlerForPayload(payloadClass); if (this.validator != null && this.defaultHandler != null) { MethodParameter parameter = this.payloadMethodParameters.get(handler); @@ -177,29 +200,29 @@ public InvocationResult invoke(Message message, Object... providedArgs) throw * @param payloadClass the payload class. * @return the handler. */ - protected InvocableHandlerMethod getHandlerForPayload(Class payloadClass) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected InvocableHandlerMethod getHandlerForPayload(Class payloadClass) { InvocableHandlerMethod handler = this.cachedHandlers.get(payloadClass); if (handler == null) { handler = findHandlerForPayload(payloadClass); if (handler == null) { - throw new AmqpException("No method found for " + payloadClass); + ReflectionUtils.rethrowRuntimeException( + new NoSuchMethodException("No listener method found in " + this.bean.getClass().getName() + + " for " + payloadClass)); } - this.cachedHandlers.putIfAbsent(payloadClass, handler); //NOSONAR + this.cachedHandlers.putIfAbsent(payloadClass, handler); setupReplyTo(handler); } return handler; } private void setupReplyTo(InvocableHandlerMethod handler) { - String replyTo = null; Method method = handler.getMethod(); - if (method != null) { - SendTo ann = AnnotationUtils.getAnnotation(method, SendTo.class); - replyTo = extractSendTo(method.toString(), ann); - } + SendTo ann = AnnotationUtils.getAnnotation(method, SendTo.class); + String replyTo = extractSendTo(method.toString(), ann); if (replyTo == null) { Class beanType = handler.getBeanType(); - SendTo ann = AnnotationUtils.getAnnotation(beanType, SendTo.class); + ann = AnnotationUtils.getAnnotation(beanType, SendTo.class); replyTo = extractSendTo(beanType.getSimpleName(), ann); } if (replyTo != null) { @@ -207,7 +230,7 @@ private void setupReplyTo(InvocableHandlerMethod handler) { } } - private String extractSendTo(String element, SendTo ann) { + private @Nullable String extractSendTo(String element, @Nullable SendTo ann) { String replyTo = null; if (ann != null) { String[] destinations = ann.value(); @@ -220,19 +243,20 @@ private String extractSendTo(String element, SendTo ann) { return replyTo; } - private String resolve(String value) { - if (this.resolver != null) { + private @Nullable String resolve(String value) { + if (this.beanExpressionContext != null) { String resolvedValue = this.beanExpressionContext.getBeanFactory().resolveEmbeddedValue(value); - Object newValue = this.resolver.evaluate(resolvedValue, this.beanExpressionContext); - Assert.isInstanceOf(String.class, newValue, "Invalid @SendTo expression"); - return (String) newValue; - } - else { - return value; + if (this.resolver != null) { + Object newValue = this.resolver.evaluate(resolvedValue, this.beanExpressionContext); + Assert.isInstanceOf(String.class, newValue, "Invalid @SendTo expression"); + return (String) newValue; + } } + + return value; } - protected InvocableHandlerMethod findHandlerForPayload(Class payloadClass) { + protected @Nullable InvocableHandlerMethod findHandlerForPayload(Class payloadClass) { InvocableHandlerMethod result = null; for (InvocableHandlerMethod handler : this.handlers) { if (matchHandlerMethod(payloadClass, handler)) { @@ -252,7 +276,7 @@ protected InvocableHandlerMethod findHandlerForPayload(Class p return result != null ? result : this.defaultHandler; } - protected boolean matchHandlerMethod(Class payloadClass, InvocableHandlerMethod handler) { + protected boolean matchHandlerMethod(Class payloadClass, InvocableHandlerMethod handler) { Method method = handler.getMethod(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); // Single param; no annotation or not @Header @@ -260,7 +284,7 @@ protected boolean matchHandlerMethod(Class payloadClass, Invoc MethodParameter methodParameter = new MethodParameter(method, 0); if ((methodParameter.getParameterAnnotations().length == 0 || !methodParameter.hasParameterAnnotation(Header.class)) - && methodParameter.getParameterType().isAssignableFrom(payloadClass)) { + && methodParameter.getParameterType().isAssignableFrom(payloadClass)) { if (this.validator != null) { this.payloadMethodParameters.put(handler, methodParameter); } @@ -270,7 +294,7 @@ protected boolean matchHandlerMethod(Class payloadClass, Invoc return findACandidate(payloadClass, handler, method, parameterAnnotations); } - private boolean findACandidate(Class payloadClass, InvocableHandlerMethod handler, Method method, + private boolean findACandidate(Class payloadClass, InvocableHandlerMethod handler, Method method, Annotation[][] parameterAnnotations) { boolean foundCandidate = false; @@ -278,7 +302,7 @@ private boolean findACandidate(Class payloadClass, InvocableHa MethodParameter methodParameter = new MethodParameter(method, i); if ((methodParameter.getParameterAnnotations().length == 0 || !methodParameter.hasParameterAnnotation(Header.class)) - && methodParameter.getParameterType().isAssignableFrom(payloadClass)) { + && methodParameter.getParameterType().isAssignableFrom(payloadClass)) { if (foundCandidate) { throw new AmqpException("Ambiguous payload parameter for " + method.toGenericString()); } @@ -321,8 +345,7 @@ public boolean hasDefaultHandler() { return this.defaultHandler != null; } - @Nullable - public InvocationResult getInvocationResultFor(Object result, Object inboundPayload) { + public @Nullable InvocationResult getInvocationResultFor(Object result, Object inboundPayload) { InvocableHandlerMethod handler = findHandlerForPayload(inboundPayload.getClass()); if (handler != null) { return new InvocationResult(result, this.handlerSendTo.get(handler), @@ -337,15 +360,12 @@ private static final class PayloadValidator extends PayloadMethodArgumentResolve super(new MessageConverter() { // Required but never used @Override - @Nullable - public Message toMessage(Object payload, @Nullable - MessageHeaders headers) { + public @Nullable Message toMessage(Object payload, @Nullable MessageHeaders headers) { return null; } @Override - @Nullable - public Object fromMessage(Message message, Class targetClass) { + public @Nullable Object fromMessage(Message message, Class targetClass) { return null; } @@ -358,4 +378,5 @@ public void validate(Message message, MethodParameter parameter, Object targe } } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java index fbc010690f..d4fcf039a6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,13 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.Assert; /** * A wrapper for either an {@link InvocableHandlerMethod} or @@ -34,9 +37,11 @@ */ public class HandlerAdapter { - private final InvocableHandlerMethod invokerHandlerMethod; + private final @Nullable InvocableHandlerMethod invokerHandlerMethod; + + private final @Nullable DelegatingInvocableHandler delegatingHandler; - private final DelegatingInvocableHandler delegatingHandler; + private final boolean asyncReplies; /** * Construct an instance with the provided method. @@ -45,6 +50,9 @@ public class HandlerAdapter { public HandlerAdapter(InvocableHandlerMethod invokerHandlerMethod) { this.invokerHandlerMethod = invokerHandlerMethod; this.delegatingHandler = null; + this.asyncReplies = (AbstractAdaptableMessageListener.monoPresent + && MonoHandler.isMono(invokerHandlerMethod.getMethod().getReturnType())) + || CompletableFuture.class.isAssignableFrom(invokerHandlerMethod.getMethod().getReturnType()); } /** @@ -54,6 +62,7 @@ public HandlerAdapter(InvocableHandlerMethod invokerHandlerMethod) { public HandlerAdapter(DelegatingInvocableHandler delegatingHandler) { this.invokerHandlerMethod = null; this.delegatingHandler = delegatingHandler; + this.asyncReplies = delegatingHandler.isAsyncReplies(); } /** @@ -63,14 +72,16 @@ public HandlerAdapter(DelegatingInvocableHandler delegatingHandler) { * @return the invocation result. * @throws Exception if one occurs. */ - public InvocationResult invoke(@Nullable Message message, Object... providedArgs) throws Exception { // NOSONAR - if (this.invokerHandlerMethod != null) { // NOSONAR (nullable message) - return new InvocationResult(this.invokerHandlerMethod.invoke(message, providedArgs), - null, this.invokerHandlerMethod.getMethod().getGenericReturnType(), - this.invokerHandlerMethod.getBean(), - this.invokerHandlerMethod.getMethod()); + public InvocationResult invoke(Message message, @Nullable Object... providedArgs) throws Exception { // NOSONAR + InvocableHandlerMethod invokerHandlerMethodToUse = this.invokerHandlerMethod; + if (invokerHandlerMethodToUse != null) { // NOSONAR (nullable message) + return new InvocationResult(invokerHandlerMethodToUse.invoke(message, providedArgs), + null, invokerHandlerMethodToUse.getMethod().getGenericReturnType(), + invokerHandlerMethodToUse.getBean(), + invokerHandlerMethodToUse.getMethod()); } - else if (this.delegatingHandler.hasDefaultHandler()) { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); + if (this.delegatingHandler.hasDefaultHandler()) { // Needed to avoid returning raw Message which matches Object Object[] args = new Object[providedArgs.length + 1]; args[0] = message.getPayload(); @@ -92,6 +103,7 @@ public String getMethodAsString(Object payload) { return this.invokerHandlerMethod.getMethod().toGenericString(); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getMethodNameFor(payload); } } @@ -107,6 +119,7 @@ public Method getMethodFor(Object payload) { return this.invokerHandlerMethod.getMethod(); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getMethodFor(payload); } } @@ -122,6 +135,7 @@ public Type getReturnTypeFor(Object payload) { return this.invokerHandlerMethod.getMethod().getReturnType(); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getMethodFor(payload).getReturnType(); } } @@ -135,10 +149,20 @@ public Object getBean() { return this.invokerHandlerMethod.getBean(); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getBean(); } } + /** + * Return true if any handler method has an async reply type. + * @return the asyncReply. + * @since 2.2.21 + */ + public boolean isAsyncReplies() { + return this.asyncReplies; + } + /** * Build an {@link InvocationResult} for the result and inbound payload. * @param result the result. @@ -146,13 +170,13 @@ public Object getBean() { * @return the invocation result. * @since 2.1.7 */ - @Nullable - public InvocationResult getInvocationResultFor(Object result, Object inboundPayload) { + public @Nullable InvocationResult getInvocationResultFor(Object result, Object inboundPayload) { if (this.invokerHandlerMethod != null) { return new InvocationResult(result, null, this.invokerHandlerMethod.getMethod().getGenericReturnType(), this.invokerHandlerMethod.getBean(), this.invokerHandlerMethod.getMethod()); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getInvocationResultFor(result, inboundPayload); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/InvocationResult.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/InvocationResult.java index feb70f89a2..414955df82 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/InvocationResult.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/InvocationResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 the original author or authors. + * Copyright 2018-2025 the original author 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,30 +19,29 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; /** * The result of a listener method invocation. * * @author Gary Russell + * @author Artem Bilan * * @since 2.1 */ public final class InvocationResult { - private final Object returnValue; + private final @Nullable Object returnValue; - private final Expression sendTo; + private final @Nullable Expression sendTo; - @Nullable - private final Type returnType; + private final @Nullable Type returnType; - @Nullable - private final Object bean; + private final @Nullable Object bean; - @Nullable - private final Method method; + private final @Nullable Method method; /** * Construct an instance with the provided properties. @@ -52,7 +51,7 @@ public final class InvocationResult { * @param bean the bean. * @param method the method. */ - public InvocationResult(Object result, @Nullable Expression sendTo, @Nullable Type returnType, + public InvocationResult(@Nullable Object result, @Nullable Expression sendTo, @Nullable Type returnType, @Nullable Object bean, @Nullable Method method) { this.returnValue = result; @@ -62,11 +61,11 @@ public InvocationResult(Object result, @Nullable Expression sendTo, @Nullable Ty this.method = method; } - public Object getReturnValue() { + public @Nullable Object getReturnValue() { return this.returnValue; } - public Expression getSendTo() { + public @Nullable Expression getSendTo() { return this.sendTo; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java new file mode 100644 index 0000000000..fd7cba2603 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java @@ -0,0 +1,51 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener.adapter; + +import java.lang.reflect.Method; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.CoroutinesUtils; +import org.springframework.core.KotlinDetector; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; + +/** + * An {@link InvocableHandlerMethod} extension for supporting Kotlin {@code suspend} function. + * + * @author Artem Bilan + * + * @since 3.0.5 + */ +public class KotlinAwareInvocableHandlerMethod extends InvocableHandlerMethod { + + public KotlinAwareInvocableHandlerMethod(Object bean, Method method) { + super(bean, method); + } + + @Override + protected @Nullable Object doInvoke(@Nullable Object... args) throws Exception { + Method method = getBridgedMethod(); + if (KotlinDetector.isSuspendingFunction(method)) { + return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args); + } + else { + return super.doInvoke(args); + } + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java index d4ef95993a..e39f632d90 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,10 @@ import java.util.HashMap; import java.util.Map; +import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpIOException; -import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessageProperties; @@ -35,12 +37,10 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import com.rabbitmq.client.Channel; - /** * Message listener adapter that delegates the handling of messages to target listener methods via reflection, with * flexible message type conversion. Allows listener methods to operate on message content types, completely independent - * from the Rabbit API. + * of the Rabbit API. * *

* By default, the content of incoming Rabbit messages gets extracted before being passed into the target listener @@ -67,6 +67,7 @@ * Message will be sent back as all of these methods return void. * *

+ * {@code
  * public interface MessageContentsDelegate {
  * 	void handleMessage(String text);
  *
@@ -76,26 +77,31 @@
  *
  * 	void handleMessage(Serializable obj);
  * }
+ * }
  * 
* * This next example handle a Message type and gets passed the actual (raw) Message as an * argument. Again, no Message will be sent back as all of these methods return void. * *
+ * {@code
  * public interface RawMessageDelegate {
  * 	void handleMessage(Message message);
  * }
+ * }
  * 
* * This next example illustrates a Message delegate that just consumes the String contents of * {@link Message Messages}. Notice also how the name of the Message handling method is different from the - * {@link #ORIGINAL_DEFAULT_LISTENER_METHOD original} (this will have to be configured in the attandant bean + * {@link #ORIGINAL_DEFAULT_LISTENER_METHOD original} (this will have to be configured in the attendant bean * definition). Again, no Message will be sent back as the method returns void. * *
+ * {@code
  * public interface TextMessageContentDelegate {
  * 	void onMessage(String text);
  * }
+ * }
  * 
* * This final example illustrates a Message delegate that just consumes the String contents of @@ -103,9 +109,11 @@ * configured {@link MessageListenerAdapter} sending a {@link Message} in response. * *
+ * {@code
  * public interface ResponsiveTextMessageContentDelegate {
  * 	String handleMessage(String text);
  * }
+ * }
  * 
* * For further examples and discussion please do refer to the Spring reference documentation which describes this class @@ -118,6 +126,8 @@ * @author Gary Russell * @author Greg Turnquist * @author Cai Kun + * @author Ngoc Nhan + * @author Artem Bilan * * @see #setDelegate * @see #setDefaultListenerMethod @@ -129,19 +139,18 @@ */ public class MessageListenerAdapter extends AbstractAdaptableMessageListener { - private final Map queueOrTagToMethodName = new HashMap(); + private final Map queueOrTagToMethodName = new HashMap<>(); /** * Out-of-the-box value for the default listener method: "handleMessage". */ public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage"; - + @SuppressWarnings("NullAway.Init") private Object delegate; private String defaultListenerMethod = ORIGINAL_DEFAULT_LISTENER_METHOD; - /** * Create a new {@link MessageListenerAdapter} with default settings. */ @@ -162,6 +171,7 @@ public MessageListenerAdapter(Object delegate) { * @param delegate the delegate object * @param messageConverter the message converter to use */ + @SuppressWarnings("this-escape") public MessageListenerAdapter(Object delegate, MessageConverter messageConverter) { doSetDelegate(delegate); super.setMessageConverter(messageConverter); @@ -218,7 +228,6 @@ protected String getDefaultListenerMethod() { return this.defaultListenerMethod; } - /** * Set the mapping of queue name or consumer tag to method name. The first lookup * is by queue name, if that returns null, we lookup by consumer tag, if that @@ -264,17 +273,17 @@ public String removeQueueOrTagToMethodName(String queueOrTag) { * @throws Exception if thrown by Rabbit API methods */ @Override - public void onMessage(Message message, Channel channel) throws Exception { // NOSONAR + public void onMessage(Message message, @Nullable Channel channel) throws Exception { // NOSONAR // Check whether the delegate is a MessageListener impl itself. // In that case, the adapter will simply act as a pass-through. Object delegateListener = getDelegate(); if (!delegateListener.equals(this)) { - if (delegateListener instanceof ChannelAwareMessageListener) { - ((ChannelAwareMessageListener) delegateListener).onMessage(message, channel); + if (delegateListener instanceof ChannelAwareMessageListener chaml) { + chaml.onMessage(message, channel); return; } - else if (delegateListener instanceof MessageListener) { - ((MessageListener) delegateListener).onMessage(message); + else if (delegateListener instanceof MessageListener messageListener) { + messageListener.onMessage(message); return; } } @@ -282,11 +291,6 @@ else if (delegateListener instanceof MessageListener) { // Regular case: find a handler method reflectively. Object convertedMessage = extractMessage(message); String methodName = getListenerMethodName(message, convertedMessage); - if (methodName == null) { - throw new AmqpIllegalStateException("No default listener method specified: " - + "Either specify a non-null value for the 'defaultListenerMethod' property or " - + "override the 'getListenerMethodName' method."); - } // Invoke the handler method with appropriate arguments. Object[] listenerArguments = buildListenerArguments(convertedMessage, channel, message); @@ -314,7 +318,7 @@ else if (delegateListener instanceof MessageListener) { * @see #setQueueOrTagToMethodName */ protected String getListenerMethodName(Message originalMessage, Object extractedMessage) { - if (this.queueOrTagToMethodName.size() > 0) { + if (!this.queueOrTagToMethodName.isEmpty()) { MessageProperties props = originalMessage.getMessageProperties(); String methodName = this.queueOrTagToMethodName.get(props.getConsumerQueue()); if (methodName == null) { @@ -343,8 +347,8 @@ protected String getListenerMethodName(Message originalMessage, Object extracted * @return the array of arguments to be passed into the listener method (each element of the array corresponding to * a distinct method argument) */ - protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { - return new Object[] { extractedMessage }; + protected Object[] buildListenerArguments(Object extractedMessage, @Nullable Channel channel, Message message) { + return new Object[] {extractedMessage}; } /** @@ -356,19 +360,23 @@ protected Object[] buildListenerArguments(Object extractedMessage, Channel chann * @see #getListenerMethodName * @see #buildListenerArguments */ - protected Object invokeListenerMethod(String methodName, Object[] arguments, Message originalMessage) { + protected @Nullable Object invokeListenerMethod(String methodName, @Nullable Object @Nullable [] arguments, + Message originalMessage) { + try { MethodInvoker methodInvoker = new MethodInvoker(); methodInvoker.setTargetObject(getDelegate()); methodInvoker.setTargetMethod(methodName); - methodInvoker.setArguments(arguments); + if (arguments != null) { + methodInvoker.setArguments(arguments); + } methodInvoker.prepare(); return methodInvoker.invoke(); } catch (InvocationTargetException ex) { Throwable targetEx = ex.getTargetException(); - if (targetEx instanceof IOException) { - throw new AmqpIOException((IOException) targetEx); // NOSONAR lost stack trace + if (targetEx instanceof IOException iox) { + throw new AmqpIOException(iox); // NOSONAR lost stack trace } else { throw new ListenerExecutionFailedException("Listener method '" // NOSONAR lost stack trace @@ -379,7 +387,7 @@ protected Object invokeListenerMethod(String methodName, Object[] arguments, Mes ArrayList arrayClass = new ArrayList<>(); if (arguments != null) { for (Object argument : arguments) { - arrayClass.add(argument.getClass().toString()); + arrayClass.add(argument != null ? argument.getClass().toString() : " null"); } } throw new ListenerExecutionFailedException("Failed to invoke target method '" + methodName diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 324bfc32b7..20ce47ea34 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,37 +20,41 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; +import java.util.Collection; import java.util.List; +import java.util.Optional; +import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.support.AmqpHeaderMapper; -import org.springframework.amqp.support.AmqpHeaders; -import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.SimpleAmqpHeaderMapper; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.MessagingMessageConverter; +import org.springframework.amqp.support.converter.RemoteInvocationResult; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.MessagingException; +import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.remoting.support.RemoteInvocationResult; import org.springframework.util.Assert; - -import com.rabbitmq.client.Channel; +import org.springframework.util.TypeUtils; /** - * A {@link org.springframework.amqp.core.MessageListener MessageListener} + * A {@link MessageListener MessageListener} * adapter that invokes a configurable {@link HandlerAdapter}. * *

Wraps the incoming {@link org.springframework.amqp.core.Message * AMQP Message} to Spring's {@link Message} abstraction, copying the * standard headers using a configurable - * {@link org.springframework.amqp.support.AmqpHeaderMapper AmqpHeaderMapper}. + * {@link AmqpHeaderMapper AmqpHeaderMapper}. * *

The original {@link org.springframework.amqp.core.Message Message} and * the {@link Channel} are provided as additional arguments so that these can @@ -65,29 +69,30 @@ */ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener { - private HandlerAdapter handlerAdapter; + protected final MessagingMessageConverterAdapter messagingMessageConverter; - private final MessagingMessageConverterAdapter messagingMessageConverter; + protected final boolean returnExceptions; - private final boolean returnExceptions; + protected final @Nullable RabbitListenerErrorHandler errorHandler; - private final RabbitListenerErrorHandler errorHandler; + private @Nullable HandlerAdapter handlerAdapter; public MessagingMessageListenerAdapter() { this(null, null); } - public MessagingMessageListenerAdapter(Object bean, Method method) { + public MessagingMessageListenerAdapter(@Nullable Object bean, @Nullable Method method) { this(bean, method, false, null); } - public MessagingMessageListenerAdapter(Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler) { + public MessagingMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler) { + this(bean, method, returnExceptions, errorHandler, false); } - protected MessagingMessageListenerAdapter(Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler, boolean batch) { + protected MessagingMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler, boolean batch) { this.messagingMessageConverter = new MessagingMessageConverterAdapter(bean, method, batch); this.returnExceptions = returnExceptions; @@ -104,15 +109,22 @@ public void setHandlerAdapter(HandlerAdapter handlerAdapter) { } protected HandlerAdapter getHandlerAdapter() { + Assert.notNull(this.handlerAdapter, "The 'handlerAdapter' is required"); return this.handlerAdapter; } + @Override + public boolean isAsyncReplies() { + Assert.notNull(this.handlerAdapter, "The 'handlerAdapter' is required"); + return this.handlerAdapter.isAsyncReplies(); + } + /** * Set the {@link AmqpHeaderMapper} implementation to use to map the standard - * AMQP headers. By default, a {@link org.springframework.amqp.support.SimpleAmqpHeaderMapper + * AMQP headers. By default, a {@link SimpleAmqpHeaderMapper * SimpleAmqpHeaderMapper} is used. * @param headerMapper the {@link AmqpHeaderMapper} instance. - * @see org.springframework.amqp.support.SimpleAmqpHeaderMapper + * @see SimpleAmqpHeaderMapper */ public void setHeaderMapper(AmqpHeaderMapper headerMapper) { Assert.notNull(headerMapper, "HeaderMapper must not be null"); @@ -121,20 +133,24 @@ public void setHeaderMapper(AmqpHeaderMapper headerMapper) { /** * @return the {@link MessagingMessageConverter} for this listener, - * being able to convert {@link org.springframework.messaging.Message}. + * being able to convert {@link Message}. */ protected final MessagingMessageConverter getMessagingMessageConverter() { return this.messagingMessageConverter; } @Override - public void setMessageConverter(MessageConverter messageConverter) { + public void setMessageConverter(@Nullable MessageConverter messageConverter) { super.setMessageConverter(messageConverter); - this.messagingMessageConverter.setPayloadConverter(messageConverter); + if (messageConverter != null) { + this.messagingMessageConverter.setPayloadConverter(messageConverter); + } } @Override - public void onMessage(org.springframework.amqp.core.Message amqpMessage, Channel channel) throws Exception { // NOSONAR + public void onMessage(org.springframework.amqp.core.Message amqpMessage, @Nullable Channel channel) + throws Exception { + Message message = null; try { message = toMessagingMessage(amqpMessage); @@ -152,23 +168,32 @@ public void onMessage(org.springframework.amqp.core.Message amqpMessage, Channel } } - private void handleException(org.springframework.amqp.core.Message amqpMessage, Channel channel, + @Override + protected void asyncFailure(org.springframework.amqp.core.Message request, @Nullable Channel channel, Throwable t, + @Nullable Object source) { + + try { + handleException(request, channel, (Message) source, + new ListenerExecutionFailedException("Async Fail", t, request)); + return; + } + catch (Exception ex) { + // Ignore + } + super.asyncFailure(request, channel, t, source); + } + + protected void handleException(org.springframework.amqp.core.Message amqpMessage, @Nullable Channel channel, @Nullable Message message, ListenerExecutionFailedException e) throws Exception { // NOSONAR if (this.errorHandler != null) { try { - Message messageWithChannel = null; - if (message != null) { - messageWithChannel = MessageBuilder.fromMessage(message) - .setHeader(AmqpHeaders.CHANNEL, channel) - .build(); - } - Object errorResult = this.errorHandler.handleError(amqpMessage, messageWithChannel, e); + Object errorResult = this.errorHandler.handleError(amqpMessage, channel, message, e); if (errorResult != null) { Object payload = message == null ? null : message.getPayload(); InvocationResult invResult = payload == null ? new InvocationResult(errorResult, null, null, null, null) - : this.handlerAdapter.getInvocationResultFor(errorResult, payload); + : getHandlerAdapter().getInvocationResultFor(errorResult, payload); handleResult(invResult, amqpMessage, channel, message); } else { @@ -184,18 +209,22 @@ private void handleException(org.springframework.amqp.core.Message amqpMessage, } } - protected void invokeHandlerAndProcessResult(@Nullable org.springframework.amqp.core.Message amqpMessage, - Channel channel, Message message) throws Exception { // NOSONAR + protected void invokeHandlerAndProcessResult(org.springframework.amqp.core.Message amqpMessage, + @Nullable Channel channel, Message message) { - if (logger.isDebugEnabled()) { + boolean projectionUsed = amqpMessage.getMessageProperties().isProjectionUsed(); + if (projectionUsed) { + amqpMessage.getMessageProperties().setProjectionUsed(false); + } + if (logger.isDebugEnabled() && !projectionUsed) { logger.debug("Processing [" + message + "]"); } - InvocationResult result = null; - if (this.messagingMessageConverter.method == null && amqpMessage != null) { + InvocationResult result; + if (this.messagingMessageConverter.method == null) { amqpMessage.getMessageProperties() - .setTargetMethod(this.handlerAdapter.getMethodFor(message.getPayload())); + .setTargetMethod(getHandlerAdapter().getMethodFor(message.getPayload())); } - result = invokeHandler(amqpMessage, channel, message); + result = invokeHandler(channel, message, false, amqpMessage); if (result.getReturnValue() != null) { handleResult(result, amqpMessage, channel, message); } @@ -204,23 +233,22 @@ protected void invokeHandlerAndProcessResult(@Nullable org.springframework.amqp. } } - private void returnOrThrow(org.springframework.amqp.core.Message amqpMessage, Channel channel, Message message, - Throwable throwableToReturn, Exception exceptionToThrow) throws Exception { // NOSONAR + private void returnOrThrow(org.springframework.amqp.core.Message amqpMessage, @Nullable Channel channel, + @Nullable Message message, @Nullable Throwable throwableToReturn, Exception exceptionToThrow) throws Exception { if (!this.returnExceptions) { throw exceptionToThrow; } Object payload = message == null ? null : message.getPayload(); try { - handleResult(new InvocationResult(new RemoteInvocationResult(throwableToReturn), null, - payload == null ? Object.class : this.handlerAdapter.getReturnTypeFor(payload), - this.handlerAdapter.getBean(), - payload == null ? null : this.handlerAdapter.getMethodFor(payload)), + payload == null ? Object.class : getHandlerAdapter().getReturnTypeFor(payload), + getHandlerAdapter().getBean(), + payload == null ? null : getHandlerAdapter().getMethodFor(payload)), amqpMessage, channel, message); } catch (ReplyFailureException rfe) { - if (payload == null || void.class.equals(this.handlerAdapter.getReturnTypeFor(payload))) { + if (payload == null || void.class.equals(getHandlerAdapter().getReturnTypeFor(payload))) { throw exceptionToThrow; } else { @@ -236,37 +264,38 @@ protected Message toMessagingMessage(org.springframework.amqp.core.Message am /** * Invoke the handler, wrapping any exception to a {@link ListenerExecutionFailedException} * with a dedicated error message. - * @param amqpMessage the raw message. + * @param amqpMessages the raw AMQP messages. * @param channel the channel. * @param message the messaging message. * @return the result of invoking the handler. */ - private InvocationResult invokeHandler(@Nullable org.springframework.amqp.core.Message amqpMessage, Channel channel, - Message message) { + protected InvocationResult invokeHandler(@Nullable Channel channel, Message message, + boolean batch, org.springframework.amqp.core.Message... amqpMessages) { try { - if (amqpMessage == null) { - return this.handlerAdapter.invoke(message, channel); + if (batch) { + return getHandlerAdapter().invoke(message, channel); } else { - return this.handlerAdapter.invoke(message, amqpMessage, channel, amqpMessage.getMessageProperties()); + org.springframework.amqp.core.Message amqpMessage = amqpMessages[0]; + return getHandlerAdapter().invoke(message, amqpMessage, channel, amqpMessage.getMessageProperties()); } } catch (MessagingException ex) { - throw new ListenerExecutionFailedException(createMessagingErrorMessage("Listener method could not " + - "be invoked with the incoming message", message.getPayload()), ex, amqpMessage); + throw new ListenerExecutionFailedException(createMessagingErrorMessage(message.getPayload()), + ex, amqpMessages); } catch (Exception ex) { throw new ListenerExecutionFailedException("Listener method '" + - this.handlerAdapter.getMethodAsString(message.getPayload()) + "' threw exception", ex, amqpMessage); + getHandlerAdapter().getMethodAsString(message.getPayload()) + "' threw exception", ex, amqpMessages); } } - private String createMessagingErrorMessage(String description, Object payload) { - return description + "\n" + private String createMessagingErrorMessage(Object payload) { + return "Listener method could not be invoked with the incoming message" + "\n" + "Endpoint handler details:\n" - + "Method [" + this.handlerAdapter.getMethodAsString(payload) + "]\n" - + "Bean [" + this.handlerAdapter.getBean() + "]"; + + "Method [" + getHandlerAdapter().getMethodAsString(payload) + "]\n" + + "Bean [" + getHandlerAdapter().getBean() + "]"; } /** @@ -278,7 +307,9 @@ private String createMessagingErrorMessage(String description, Object payload) { * @see #setMessageConverter */ @Override - protected org.springframework.amqp.core.Message buildMessage(Channel channel, Object result, Type genericType) { + protected org.springframework.amqp.core.Message buildMessage(@Nullable Channel channel, @Nullable Object result, + @Nullable Type genericType) { + MessageConverter converter = getMessageConverter(); if (converter != null && !(result instanceof org.springframework.amqp.core.Message)) { if (result instanceof org.springframework.messaging.Message) { @@ -289,11 +320,13 @@ protected org.springframework.amqp.core.Message buildMessage(Channel channel, Ob } } else { - if (!(result instanceof org.springframework.amqp.core.Message)) { + if (result instanceof org.springframework.amqp.core.Message msg) { + return msg; + } + else { throw new MessageConversionException("No MessageConverter specified - cannot handle message [" + result + "]"); } - return (org.springframework.amqp.core.Message) result; } } @@ -307,11 +340,11 @@ protected org.springframework.amqp.core.Message buildMessage(Channel channel, Ob */ protected final class MessagingMessageConverterAdapter extends MessagingMessageConverter { - private final Object bean; + private final @Nullable Object bean; - final Method method; // NOSONAR visibility + final @Nullable Method method; // NOSONAR visibility - private final Type inferredArgumentType; + private final @Nullable Type inferredArgumentType; private final boolean isBatch; @@ -319,25 +352,27 @@ protected final class MessagingMessageConverterAdapter extends MessagingMessageC private boolean isAmqpMessageList; - MessagingMessageConverterAdapter(Object bean, Method method, boolean batch) { + private boolean isCollection; + + MessagingMessageConverterAdapter(@Nullable Object bean, @Nullable Method method, boolean batch) { this.bean = bean; this.method = method; this.isBatch = batch; this.inferredArgumentType = determineInferredType(); if (logger.isDebugEnabled() && this.inferredArgumentType != null) { - logger.debug("Inferred argument type for " + method.toString() + " is " + this.inferredArgumentType); + logger.debug("Inferred argument type for " + method + " is " + this.inferredArgumentType); } } - protected boolean isMessageList() { + public boolean isMessageList() { return this.isMessageList; } - protected boolean isAmqpMessageList() { + public boolean isAmqpMessageList() { return this.isAmqpMessageList; } - protected Method getMethod() { + public @Nullable Method getMethod() { return this.method; } @@ -356,7 +391,7 @@ protected Object extractPayload(org.springframework.amqp.core.Message message) { return extractMessage(message); } - private Type determineInferredType() { // NOSONAR - complexity + private @Nullable Type determineInferredType() { // NOSONAR - complexity if (this.method == null) { return null; } @@ -370,18 +405,24 @@ private Type determineInferredType() { // NOSONAR - complexity * We ignore parameters with type Message because they are not involved with conversion. */ boolean isHeaderOrHeaders = methodParameter.hasParameterAnnotation(Header.class) - || methodParameter.hasParameterAnnotation(Headers.class); + || methodParameter.hasParameterAnnotation(Headers.class) + || methodParameter.getParameterType().equals(MessageHeaders.class); boolean isPayload = methodParameter.hasParameterAnnotation(Payload.class); if (isHeaderOrHeaders && isPayload && MessagingMessageListenerAdapter.this.logger.isWarnEnabled()) { MessagingMessageListenerAdapter.this.logger.warn(this.method.getName() - + ": Cannot annotate a parameter with both @Header and @Payload; " - + "ignored for payload conversion"); + + ": Cannot annotate a parameter with both @Header and @Payload; " + + "ignored for payload conversion"); } - if (isEligibleParameter(methodParameter) // NOSONAR - && (!isHeaderOrHeaders || isPayload) && !(isHeaderOrHeaders && isPayload)) { + if (isEligibleParameter(methodParameter) && !isHeaderOrHeaders) { if (genericParameterType == null) { genericParameterType = extractGenericParameterTypFromMethodParameter(methodParameter); + if (this.isBatch && !this.isCollection) { + throw new IllegalStateException( + "Mis-configuration; a batch listener must consume a List or " + + "Collection for method: " + this.method); + } + } else { if (MessagingMessageListenerAdapter.this.logger.isDebugEnabled()) { @@ -393,7 +434,13 @@ private Type determineInferredType() { // NOSONAR - complexity } } } + return checkOptional(genericParameterType); + } + protected @Nullable Type checkOptional(@Nullable Type genericParameterType) { + if (genericParameterType instanceof ParameterizedType pType && pType.getRawType().equals(Optional.class)) { + return pType.getActualTypeArguments()[0]; + } return genericParameterType; } @@ -405,34 +452,35 @@ private boolean isEligibleParameter(MethodParameter methodParameter) { Type parameterType = methodParameter.getGenericParameterType(); if (parameterType.equals(Channel.class) || parameterType.equals(MessageProperties.class) - || parameterType.equals(org.springframework.amqp.core.Message.class)) { + || parameterType.equals(org.springframework.amqp.core.Message.class) + || parameterType.getTypeName().startsWith("kotlin.coroutines.Continuation")) { return false; } - if (parameterType instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) parameterType; - if (parameterizedType.getRawType().equals(Message.class)) { - return !(parameterizedType.getActualTypeArguments()[0] instanceof WildcardType); - } + if (parameterType instanceof ParameterizedType parameterizedType && + (parameterizedType.getRawType().equals(Message.class))) { + return !(parameterizedType.getActualTypeArguments()[0] instanceof WildcardType); } return !parameterType.equals(Message.class); // could be Message without a generic type } private Type extractGenericParameterTypFromMethodParameter(MethodParameter methodParameter) { Type genericParameterType = methodParameter.getGenericParameterType(); - if (genericParameterType instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) genericParameterType; + if (genericParameterType instanceof ParameterizedType parameterizedType) { if (parameterizedType.getRawType().equals(Message.class)) { genericParameterType = ((ParameterizedType) genericParameterType).getActualTypeArguments()[0]; } - else if (this.isBatch - && parameterizedType.getRawType().equals(List.class) - && parameterizedType.getActualTypeArguments().length == 1) { + else if (this.isBatch && + (parameterizedType.getRawType().equals(List.class) || + (parameterizedType.getRawType().equals(Collection.class) && + parameterizedType.getActualTypeArguments().length == 1))) { + this.isCollection = true; Type paramType = parameterizedType.getActualTypeArguments()[0]; - boolean messageHasGeneric = paramType instanceof ParameterizedType - && ((ParameterizedType) paramType).getRawType().equals(Message.class); - this.isMessageList = paramType.equals(Message.class) || messageHasGeneric; - this.isAmqpMessageList = paramType.equals(org.springframework.amqp.core.Message.class); + boolean messageHasGeneric = paramType instanceof ParameterizedType pType + && pType.getRawType().equals(Message.class); + this.isMessageList = TypeUtils.isAssignable(paramType, Message.class) || messageHasGeneric; + this.isAmqpMessageList = + TypeUtils.isAssignable(paramType, org.springframework.amqp.core.Message.class); if (messageHasGeneric) { genericParameterType = ((ParameterizedType) paramType).getActualTypeArguments()[0]; } @@ -444,6 +492,7 @@ else if (this.isBatch } return genericParameterType; } + } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java new file mode 100644 index 0000000000..4293a64c86 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener.adapter; + +import java.util.function.Consumer; + +import org.jspecify.annotations.Nullable; +import reactor.core.publisher.Mono; + +/** + * Class to prevent direct links to {@link Mono}. + * + * @author Gary Russell + * + * @since 2.2.21 + */ +final class MonoHandler { // NOSONAR - pointless to name it ..Utils|Helper + + private MonoHandler() { + } + + static boolean isMono(@Nullable Object result) { + return result instanceof Mono; + } + + static boolean isMono(Class resultType) { + return Mono.class.isAssignableFrom(resultType); + } + + @SuppressWarnings("unchecked") + static void subscribe(Object returnValue, @Nullable Consumer success, + Consumer failure, Runnable completeConsumer) { + + ((Mono) returnValue).subscribe(success, failure, completeConsumer); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/package-info.java index 09c7c25b90..6322c6ef95 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes for adapting listeners. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener.adapter; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java index f80cad7523..468ab9b752 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,10 @@ import java.util.List; -import org.springframework.amqp.core.Message; - import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Message; /** * Used to receive a batch of messages if the container supports it. @@ -32,11 +33,11 @@ public interface ChannelAwareBatchMessageListener extends ChannelAwareMessageListener { @Override - default void onMessage(Message message, Channel channel) throws Exception { + default void onMessage(Message message, @Nullable Channel channel) throws Exception { throw new UnsupportedOperationException("Should never be called by the container"); } @Override - void onMessageBatch(List messages, Channel channel); + void onMessageBatch(List messages, @Nullable Channel channel); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java index 3ab039d2e2..6f38e2535b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,11 @@ import java.util.List; +import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; -import org.springframework.lang.Nullable; - -import com.rabbitmq.client.Channel; /** * A message listener that is aware of the Channel on which the message was received. @@ -50,7 +50,7 @@ default void onMessage(Message message) { } @SuppressWarnings("unused") - default void onMessageBatch(List messages, Channel channel) { + default void onMessageBatch(List messages, @Nullable Channel channel) { throw new UnsupportedOperationException("This listener does not support message batches"); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java index 745e351d31..3c4112f6c6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ package org.springframework.amqp.rabbit.listener.api; +import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; -import org.springframework.lang.Nullable; /** * An error handler which is called when a {code @RabbitListener} method @@ -26,6 +28,8 @@ * listener container's error handler. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 * */ @@ -36,13 +40,17 @@ public interface RabbitListenerErrorHandler { * Handle the error. If an exception is not thrown, the return value is returned to * the sender using normal {@code replyTo/@SendTo} semantics. * @param amqpMessage the raw message received. + * @param channel AMQP channel for manual acks. * @param message the converted spring-messaging message (if available). * @param exception the exception the listener threw, wrapped in a * {@link ListenerExecutionFailedException}. * @return the return value to be sent to the sender. * @throws Exception an exception which may be the original or different. + * @since 3.1.3 */ - Object handleError(Message amqpMessage, @Nullable org.springframework.messaging.Message message, - ListenerExecutionFailedException exception) throws Exception; // NOSONAR + @Nullable + Object handleError(Message amqpMessage, @Nullable Channel channel, + org.springframework.messaging.@Nullable Message message, + ListenerExecutionFailedException exception) throws Exception; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/package-info.java index c25b7c53fe..7ec1bd84de 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/package-info.java @@ -1,4 +1,5 @@ /** * Provides Additional APIs for listeners. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener.api; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/exception/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/exception/package-info.java index c6351b3ec2..b796af83b4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/exception/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/exception/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes for listener exceptions. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener.exception; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/package-info.java index c06b50e425..979ee9415a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for message listener containers. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java index cf88726675..f57094799a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author 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.amqp.rabbit.listener.support; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.ImmediateRequeueAmqpException; import org.springframework.amqp.rabbit.listener.exception.MessageRejectedWhileStoppingException; @@ -59,7 +61,11 @@ else if (t instanceof ImmediateRequeueAmqpException) { shouldRequeue = true; break; } - t = t.getCause(); + Throwable cause = t.getCause(); + if (cause == t) { + break; + } + t = cause; } if (logger.isDebugEnabled()) { logger.debug("Rejecting messages (requeue=" + shouldRequeue + ")"); @@ -75,8 +81,41 @@ else if (t instanceof ImmediateRequeueAmqpException) { * @since 2.2 */ public static boolean isRejectManual(Throwable ex) { - return ex instanceof AmqpRejectAndDontRequeueException - && ((AmqpRejectAndDontRequeueException) ex).isRejectManual(); + AmqpRejectAndDontRequeueException amqpRejectAndDontRequeueException = + findInCause(ex, AmqpRejectAndDontRequeueException.class); + return amqpRejectAndDontRequeueException != null && amqpRejectAndDontRequeueException.isRejectManual(); + } + + /** + * Return true for {@link ImmediateAcknowledgeAmqpException}. + * @param ex the exception to traverse. + * @return true if an {@link ImmediateAcknowledgeAmqpException} is present in the cause chain. + * @since 4.0 + */ + public static boolean isImmediateAcknowledge(Throwable ex) { + return findInCause(ex, ImmediateAcknowledgeAmqpException.class) != null; + } + + /** + * Return true for {@link AmqpRejectAndDontRequeueException}. + * @param ex the exception to traverse. + * @return true if an {@link AmqpRejectAndDontRequeueException} is present in the cause chain. + * @since 4.0 + */ + public static boolean isAmqpReject(Throwable ex) { + return findInCause(ex, AmqpRejectAndDontRequeueException.class) != null; + } + + @SuppressWarnings("unchecked") + private static @Nullable T findInCause(Throwable throwable, Class exceptionToFind) { + if (exceptionToFind.isAssignableFrom(throwable.getClass())) { + return (T) throwable; + } + Throwable cause = throwable.getCause(); + if (cause == null || cause == throwable) { + return null; + } + return findInCause(cause, exceptionToFind); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/package-info.java index 3f0670c263..bc14995a9c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/package-info.java @@ -1,4 +1,5 @@ /** * Provides support classes for listeners. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener.support; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java index de4888660f..f56449bfd0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author 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,10 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import com.rabbitmq.client.ConnectionFactory; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.Layout; @@ -51,6 +54,7 @@ import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.layout.PatternLayout; import org.apache.logging.log4j.core.util.Integers; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpException; @@ -83,8 +87,6 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.rabbitmq.client.ConnectionFactory; - /** * A Log4j 2 appender that publishes logging events to an AMQP Exchange. * @@ -139,7 +141,7 @@ public class AmqpAppender extends AbstractAppender { /** * Used to synchronize access to pattern layouts. */ - private final Object layoutMutex = new Object(); + private final Lock layoutMutex = new ReentrantLock(); /** * Construct an instance with the provided properties. @@ -151,8 +153,8 @@ public class AmqpAppender extends AbstractAppender { * @param eventQueue the event queue. * @param properties the properties. */ - public AmqpAppender(String name, Filter filter, Layout layout, boolean ignoreExceptions, - Property[] properties, AmqpManager manager, BlockingQueue eventQueue) { + public AmqpAppender(String name, @Nullable Filter filter, Layout layout, + boolean ignoreExceptions, Property[] properties, AmqpManager manager, BlockingQueue eventQueue) { super(name, filter, layout, ignoreExceptions, properties); this.manager = manager; @@ -226,7 +228,7 @@ protected void sendEvent(Event event, Map properties) { } // Set applicationId, if we're using one - if (null != this.manager.applicationId) { + if (this.manager.applicationId != null) { amqpProps.setAppId(this.manager.applicationId); } @@ -256,19 +258,20 @@ protected void doSend(Event event, LogEvent logEvent, MessageProperties amqpProp StringBuilder msgBody; String routingKey; try { - synchronized (this.layoutMutex) { + this.layoutMutex.lock(); + try { msgBody = new StringBuilder(new String(getLayout().toByteArray(logEvent), StandardCharsets.UTF_8)); routingKey = new String(this.manager.routingKeyLayout.toByteArray(logEvent), StandardCharsets.UTF_8); } + finally { + this.layoutMutex.unlock(); + } Message message = null; - if (this.manager.charset != null) { - try { - message = new Message(msgBody.toString().getBytes(this.manager.charset), - amqpProps); - } - catch (UnsupportedEncodingException e) { - /* fall back to default */ - } + try { + message = new Message(msgBody.toString().getBytes(this.manager.charset), amqpProps); + } + catch (UnsupportedEncodingException e) { + /* fall back to default */ } if (message == null) { message = new Message(msgBody.toString().getBytes(), amqpProps); //NOSONAR (default charset) @@ -340,7 +343,7 @@ public void run() { /** * Helper class to encapsulate a LoggingEvent, its MDC properties, and the number of retries. */ - protected static class Event { + public static class Event { private final LogEvent event; @@ -370,7 +373,7 @@ public int incrementRetries() { /** * Manager class for the appender. */ - protected static class AmqpManager extends AbstractManager { + public static class AmqpManager extends AbstractManager { private static final int DEFAULT_MAX_SENDER_RETRIES = 30; @@ -399,12 +402,13 @@ protected static class AmqpManager extends AbstractManager { /** * Log4J Layout to use to generate routing key. */ + @SuppressWarnings("NullAway.Init") private Layout routingKeyLayout; /** * Configuration arbitrary application ID. */ - private String applicationId = null; + private @Nullable String applicationId; /** * How many senders to use at once. Use more senders if you have lots of log output going through this appender. @@ -419,42 +423,43 @@ protected static class AmqpManager extends AbstractManager { /** * RabbitMQ ConnectionFactory. */ + @SuppressWarnings("NullAway.Init") private AbstractConnectionFactory connectionFactory; /** * RabbitMQ host to connect to. */ - private URI uri; + private @Nullable URI uri; /** * RabbitMQ host to connect to. */ - private String host; + private @Nullable String host; /** * A comma-delimited list of broker addresses: host:port[,host:port]*. */ - private String addresses; + private @Nullable String addresses; /** * RabbitMQ virtual host to connect to. */ - private String virtualHost; + private @Nullable String virtualHost; /** * RabbitMQ port to connect to. */ - private Integer port; + private @Nullable Integer port; /** * RabbitMQ user to connect as. */ - private String username; + private @Nullable String username; /** * RabbitMQ password for this user. */ - private String password; + private @Nullable String password; /** * Use an SSL connection. @@ -466,22 +471,22 @@ protected static class AmqpManager extends AbstractManager { /** * The SSL algorithm to use. */ - private String sslAlgorithm; + private @Nullable String sslAlgorithm; /** * Location of resource containing keystore and truststore information. */ - private String sslPropertiesLocation; + private @Nullable String sslPropertiesLocation; /** * Keystore location. */ - private String keyStore; + private @Nullable String keyStore; /** * Keystore passphrase. */ - private String keyStorePassphrase; + private @Nullable String keyStorePassphrase; /** * Keystore type. @@ -491,12 +496,12 @@ protected static class AmqpManager extends AbstractManager { /** * Truststore location. */ - private String trustStore; + private @Nullable String trustStore; /** * Truststore passphrase. */ - private String trustStorePassphrase; + private @Nullable String trustStorePassphrase; /** * Truststore type. @@ -507,7 +512,7 @@ protected static class AmqpManager extends AbstractManager { * SaslConfig. * @see RabbitUtils#stringToSaslConfig(String, ConnectionFactory) */ - private String saslConfig; + private @Nullable String saslConfig; /** * Default content-type of log messages. @@ -517,23 +522,23 @@ protected static class AmqpManager extends AbstractManager { /** * Default content-encoding of log messages. */ - private String contentEncoding = null; + private @Nullable String contentEncoding; /** - * Whether or not to try and declare the configured exchange when this appender starts. + * Whether to try and declare the configured exchange when this appender starts. */ private boolean declareExchange = false; /** * A name for the connection (appears on the RabbitMQ Admin UI). */ - private String connectionName; + private @Nullable String connectionName; /** * Additional client connection properties to be added to the rabbit connection, * with the form {@code key:value[,key:value]...}. */ - private String clientConnectionProperties; + private @Nullable String clientConnectionProperties; /** * charset to use when converting String to byte[], default null (system default charset used). @@ -543,7 +548,7 @@ protected static class AmqpManager extends AbstractManager { private String charset = Charset.defaultCharset().name(); /** - * Whether or not add MDC properties into message headers. true by default for backward compatibility + * Whether add MDC properties into message headers. true by default for backward compatibility */ private boolean addMdcAsHeaders = true; @@ -561,7 +566,8 @@ protected static class AmqpManager extends AbstractManager { /** * The pool of senders. */ - private ExecutorService senderPool = null; + @SuppressWarnings("NullAway.Init") + private ExecutorService senderPool; /** * Retries are delayed like: N ^ log(N), where N is the retry number. @@ -572,6 +578,7 @@ protected AmqpManager(LoggerContext loggerContext, String name) { super(loggerContext, name); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private boolean activateOptions() { ConnectionFactory rabbitConnectionFactory = createRabbitConnectionFactory(); if (rabbitConnectionFactory != null) { @@ -590,10 +597,8 @@ private boolean activateOptions() { if (this.addresses != null) { this.connectionFactory.setAddresses(this.addresses); } - if (this.clientConnectionProperties != null) { - ConnectionFactoryConfigurationUtils.updateClientConnectionProperties(this.connectionFactory, + ConnectionFactoryConfigurationUtils.updateClientConnectionProperties(this.connectionFactory, this.clientConnectionProperties); - } setUpExchangeDeclaration(); this.senderPool = Executors.newCachedThreadPool(); return true; @@ -603,10 +608,9 @@ private boolean activateOptions() { /** * Create the {@link ConnectionFactory}. - * * @return a {@link ConnectionFactory}. */ - protected ConnectionFactory createRabbitConnectionFactory() { + protected @Nullable ConnectionFactory createRabbitConnectionFactory() { RabbitConnectionFactoryBean factoryBean = new RabbitConnectionFactoryBean(); configureRabbitConnectionFactory(factoryBean); try { @@ -686,65 +690,56 @@ protected boolean releaseSub(long timeout, TimeUnit timeUnit) { protected void setUpExchangeDeclaration() { RabbitAdmin admin = new RabbitAdmin(this.connectionFactory); if (this.declareExchange) { - Exchange x; - if ("topic".equals(this.exchangeType)) { - x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("direct".equals(this.exchangeType)) { - x = new DirectExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("fanout".equals(this.exchangeType)) { - x = new FanoutExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("headers".equals(this.exchangeType)) { - x = new HeadersExchange(this.exchangeName, this.durable, this.autoDelete); - } - else { - x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete); - } + Exchange x = switch (this.exchangeType) { + case "direct" -> new DirectExchange(this.exchangeName, this.durable, this.autoDelete); + case "fanout" -> new FanoutExchange(this.exchangeName, this.durable, this.autoDelete); + case "headers" -> new HeadersExchange(this.exchangeName, this.durable, this.autoDelete); + default -> new TopicExchange(this.exchangeName, this.durable, this.autoDelete); + }; this.connectionFactory.addConnectionListener(new DeclareExchangeConnectionListener(x, admin)); } } } - protected static class Builder implements org.apache.logging.log4j.core.util.Builder { + public static class Builder implements org.apache.logging.log4j.core.util.Builder { @PluginConfiguration + @SuppressWarnings("NullAway.Init") private Configuration configuration; @PluginBuilderAttribute("name") - private String name; + private @Nullable String name; @PluginElement("Layout") - private Layout layout; + private @Nullable Layout layout; @PluginElement("Filter") - private Filter filter; + private @Nullable Filter filter; @PluginBuilderAttribute("ignoreExceptions") private boolean ignoreExceptions; @PluginBuilderAttribute("uri") - private URI uri; + private @Nullable URI uri; @PluginBuilderAttribute("host") - private String host; + private @Nullable String host; @PluginBuilderAttribute("port") - private String port; + private @Nullable String port; @PluginBuilderAttribute("addresses") - private String addresses; + private @Nullable String addresses; @PluginBuilderAttribute("user") - private String user; + private @Nullable String user; @PluginBuilderAttribute("password") - private String password; + private @Nullable String password; @PluginBuilderAttribute("virtualHost") - private String virtualHost; + private @Nullable String virtualHost; @PluginBuilderAttribute("useSsl") private boolean useSsl; @@ -753,31 +748,31 @@ protected static class Builder implements org.apache.logging.log4j.core.util.Bui private boolean verifyHostname; @PluginBuilderAttribute("sslAlgorithm") - private String sslAlgorithm; + private @Nullable String sslAlgorithm; @PluginBuilderAttribute("sslPropertiesLocation") - private String sslPropertiesLocation; + private @Nullable String sslPropertiesLocation; @PluginBuilderAttribute("keyStore") - private String keyStore; + private @Nullable String keyStore; @PluginBuilderAttribute("keyStorePassphrase") - private String keyStorePassphrase; + private @Nullable String keyStorePassphrase; @PluginBuilderAttribute("keyStoreType") - private String keyStoreType; + private @Nullable String keyStoreType; @PluginBuilderAttribute("trustStore") - private String trustStore; + private @Nullable String trustStore; @PluginBuilderAttribute("trustStorePassphrase") - private String trustStorePassphrase; + private @Nullable String trustStorePassphrase; @PluginBuilderAttribute("trustStoreType") - private String trustStoreType; + private @Nullable String trustStoreType; @PluginBuilderAttribute("saslConfig") - private String saslConfig; + private @Nullable String saslConfig; @PluginBuilderAttribute("senderPoolSize") private int senderPoolSize; @@ -786,22 +781,22 @@ protected static class Builder implements org.apache.logging.log4j.core.util.Bui private int maxSenderRetries; @PluginBuilderAttribute("applicationId") - private String applicationId; + private @Nullable String applicationId; @PluginBuilderAttribute("routingKeyPattern") - private String routingKeyPattern; + private @Nullable String routingKeyPattern; @PluginBuilderAttribute("generateId") private boolean generateId; @PluginBuilderAttribute("deliveryMode") - private String deliveryMode; + private @Nullable String deliveryMode; @PluginBuilderAttribute("exchange") - private String exchange; + private @Nullable String exchange; @PluginBuilderAttribute("exchangeType") - private String exchangeType; + private @Nullable String exchangeType; @PluginBuilderAttribute("declareExchange") private boolean declareExchange; @@ -813,28 +808,28 @@ protected static class Builder implements org.apache.logging.log4j.core.util.Bui private boolean autoDelete; @PluginBuilderAttribute("contentType") - private String contentType; + private @Nullable String contentType; @PluginBuilderAttribute("contentEncoding") - private String contentEncoding; + private @Nullable String contentEncoding; @PluginBuilderAttribute("connectionName") - private String connectionName; + private @Nullable String connectionName; @PluginBuilderAttribute("clientConnectionProperties") - private String clientConnectionProperties; + private @Nullable String clientConnectionProperties; @PluginBuilderAttribute("async") private boolean async; @PluginBuilderAttribute("charset") - private String charset; + private @Nullable String charset; @PluginBuilderAttribute("bufferSize") private int bufferSize = Integer.MAX_VALUE; @PluginElement(BlockingQueueFactory.ELEMENT_TYPE) - private BlockingQueueFactory blockingQueueFactory; + private @Nullable BlockingQueueFactory blockingQueueFactory; @PluginBuilderAttribute("addMdcAsHeaders") private boolean addMdcAsHeaders = Boolean.TRUE; @@ -1055,7 +1050,8 @@ public Builder setAddMdcAsHeaders(boolean addMdcAsHeaders) { } @Override - public AmqpAppender build() { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public @Nullable AmqpAppender build() { if (this.name == null) { LOGGER.error("No name for AmqpAppender"); } @@ -1088,7 +1084,8 @@ public AmqpAppender build() { .acceptIfNotNull(this.applicationId, value -> manager.applicationId = value) .acceptIfNotNull(this.routingKeyPattern, value -> manager.routingKeyPattern = value) .acceptIfNotNull(this.generateId, value -> manager.generateId = value) - .acceptIfNotNull(this.deliveryMode, value -> manager.deliveryMode = MessageDeliveryMode.valueOf(this.deliveryMode)) + .acceptIfNotNull(this.deliveryMode, + value -> manager.deliveryMode = MessageDeliveryMode.valueOf(this.deliveryMode)) .acceptIfNotNull(this.exchange, value -> manager.exchangeName = value) .acceptIfNotNull(this.exchangeType, value -> manager.exchangeType = value) .acceptIfNotNull(this.declareExchange, value -> manager.declareExchange = value) @@ -1110,7 +1107,8 @@ public AmqpAppender build() { eventQueue = this.blockingQueueFactory.create(this.bufferSize); } - AmqpAppender appender = buildInstance(this.name, this.filter, theLayout, this.ignoreExceptions, manager, eventQueue); + AmqpAppender appender = + buildInstance(this.name, this.filter, theLayout, this.ignoreExceptions, manager, eventQueue); if (manager.activateOptions()) { appender.startSenders(); return appender; @@ -1119,7 +1117,7 @@ public AmqpAppender build() { } /** - * Subclasses can extends Builder, use same logic but need to modify class instance. + * Subclasses can extend Builder, use same logic but need to modify class instance. * * @param name The Appender name. * @param filter The Filter to associate with the Appender. @@ -1130,7 +1128,7 @@ public AmqpAppender build() { * @param eventQueue Where LoggingEvents are queued to send. * @return {@link AmqpAppender} */ - protected AmqpAppender buildInstance(String name, Filter filter, Layout layout, + protected AmqpAppender buildInstance(String name, @Nullable Filter filter, Layout layout, boolean ignoreExceptions, AmqpManager manager, BlockingQueue eventQueue) { return new AmqpAppender(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY, manager, eventQueue); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/package-info.java index e8fa832998..b68d9af973 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting Log4j 2 appenders. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.log4j2; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java index be35f98c0b..f7a6616b6f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author 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,17 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.pattern.TargetLengthBasedClassNameAbbreviator; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.Layout; +import ch.qos.logback.core.encoder.Encoder; +import com.rabbitmq.client.ConnectionFactory; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.DirectExchange; @@ -59,16 +70,6 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.util.StringUtils; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.PatternLayout; -import ch.qos.logback.classic.pattern.TargetLengthBasedClassNameAbbreviator; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.classic.spi.LoggingEvent; -import ch.qos.logback.core.AppenderBase; -import ch.qos.logback.core.Layout; -import ch.qos.logback.core.encoder.Encoder; -import com.rabbitmq.client.ConnectionFactory; - /** * A Logback appender that publishes logging events to an AMQP Exchange. *

@@ -157,17 +158,18 @@ public class AmqpAppender extends AppenderBase { /** * Configuration arbitrary application ID. */ - private String applicationId = null; + private @Nullable String applicationId = null; /** * Where LoggingEvents are queued to send. */ + @SuppressWarnings("NullAway.Init") private BlockingQueue events; /** * The pool of senders. */ - private ExecutorService senderPool = null; + private @Nullable ExecutorService senderPool; /** * How many senders to use at once. Use more senders if you have lots of log output going through this appender. @@ -187,55 +189,56 @@ public class AmqpAppender extends AppenderBase { /** * RabbitMQ ConnectionFactory. */ + @SuppressWarnings("NullAway.Init") private AbstractConnectionFactory connectionFactory; /** * A name for the connection (appears on the RabbitMQ Admin UI). */ - private String connectionName; + private @Nullable String connectionName; /** * Additional client connection properties added to the rabbit connection, with the form * {@code key:value[,key:value]...}. */ - private String clientConnectionProperties; + private @Nullable String clientConnectionProperties; /** * A comma-delimited list of broker addresses: host:port[,host:port]* * * @since 1.5.6 */ - private String addresses; + private @Nullable String addresses; /** * RabbitMQ host to connect to. */ - private URI uri; + private @Nullable URI uri; /** * RabbitMQ host to connect to. */ - private String host; + private @Nullable String host; /** * RabbitMQ virtual host to connect to. */ - private String virtualHost; + private @Nullable String virtualHost; /** * RabbitMQ port to connect to. */ - private Integer port; + private @Nullable Integer port; /** * RabbitMQ user to connect as. */ - private String username; + private @Nullable String username; /** * RabbitMQ password for this user. */ - private String password; + private @Nullable String password; /** * Use an SSL connection. @@ -245,22 +248,22 @@ public class AmqpAppender extends AppenderBase { /** * The SSL algorithm to use. */ - private String sslAlgorithm; + private @Nullable String sslAlgorithm; /** * Location of resource containing keystore and truststore information. */ - private String sslPropertiesLocation; + private @Nullable String sslPropertiesLocation; /** * Keystore location. */ - private String keyStore; + private @Nullable String keyStore; /** * Keystore passphrase. */ - private String keyStorePassphrase; + private @Nullable String keyStorePassphrase; /** * Keystore type. @@ -270,12 +273,12 @@ public class AmqpAppender extends AppenderBase { /** * Truststore location. */ - private String trustStore; + private @Nullable String trustStore; /** * Truststore passphrase. */ - private String trustStorePassphrase; + private @Nullable String trustStorePassphrase; /** * Truststore type. @@ -286,7 +289,7 @@ public class AmqpAppender extends AppenderBase { * SaslConfig. * @see RabbitUtils#stringToSaslConfig(String, ConnectionFactory) */ - private String saslConfig; + private @Nullable String saslConfig; private boolean verifyHostname = true; @@ -298,10 +301,10 @@ public class AmqpAppender extends AppenderBase { /** * Default content-encoding of log messages. */ - private String contentEncoding = null; + private @Nullable String contentEncoding = null; /** - * Whether or not to try and declare the configured exchange when this appender starts. + * Whether to try and declare the configured exchange when this appender starts. */ private boolean declareExchange = false; @@ -310,10 +313,10 @@ public class AmqpAppender extends AppenderBase { * If the charset is unsupported on the current platform, we fall back to using * the system charset. */ - private String charset; + private @Nullable String charset; /** - * Whether or not add MDC properties into message headers. true by default for backward compatibility + * Whether add MDC properties into message headers. true by default for backward compatibility */ private boolean addMdcAsHeaders = true; @@ -328,11 +331,11 @@ public class AmqpAppender extends AppenderBase { */ private boolean generateId = false; - private Layout layout; + private @Nullable Layout layout; - private Encoder encoder; + private @Nullable Encoder encoder; - private TargetLengthBasedClassNameAbbreviator abbreviator; + private @Nullable TargetLengthBasedClassNameAbbreviator abbreviator; private boolean includeCallerData; @@ -340,7 +343,7 @@ public void setRoutingKeyPattern(String routingKeyPattern) { this.routingKeyLayout.setPattern("%nopex{}" + routingKeyPattern); } - public URI getUri() { + public @Nullable URI getUri() { return this.uri; } @@ -348,7 +351,7 @@ public void setUri(URI uri) { this.uri = uri; } - public String getHost() { + public @Nullable String getHost() { return this.host; } @@ -356,7 +359,7 @@ public void setHost(String host) { this.host = host; } - public Integer getPort() { + public @Nullable Integer getPort() { return this.port; } @@ -368,11 +371,11 @@ public void setAddresses(String addresses) { this.addresses = addresses; } - public String getAddresses() { + public @Nullable String getAddresses() { return this.addresses; } - public String getVirtualHost() { + public @Nullable String getVirtualHost() { return this.virtualHost; } @@ -380,7 +383,7 @@ public void setVirtualHost(String virtualHost) { this.virtualHost = virtualHost; } - public String getUsername() { + public @Nullable String getUsername() { return this.username; } @@ -388,7 +391,7 @@ public void setUsername(String username) { this.username = username; } - public String getPassword() { + public @Nullable String getPassword() { return this.password; } @@ -423,7 +426,7 @@ public boolean isVerifyHostname() { return this.verifyHostname; } - public String getSslAlgorithm() { + public @Nullable String getSslAlgorithm() { return this.sslAlgorithm; } @@ -431,7 +434,7 @@ public void setSslAlgorithm(String sslAlgorithm) { this.sslAlgorithm = sslAlgorithm; } - public String getSslPropertiesLocation() { + public @Nullable String getSslPropertiesLocation() { return this.sslPropertiesLocation; } @@ -439,7 +442,7 @@ public void setSslPropertiesLocation(String sslPropertiesLocation) { this.sslPropertiesLocation = sslPropertiesLocation; } - public String getKeyStore() { + public @Nullable String getKeyStore() { return this.keyStore; } @@ -447,7 +450,7 @@ public void setKeyStore(String keyStore) { this.keyStore = keyStore; } - public String getKeyStorePassphrase() { + public @Nullable String getKeyStorePassphrase() { return this.keyStorePassphrase; } @@ -463,7 +466,7 @@ public void setKeyStoreType(String keyStoreType) { this.keyStoreType = keyStoreType; } - public String getTrustStore() { + public @Nullable String getTrustStore() { return this.trustStore; } @@ -471,7 +474,7 @@ public void setTrustStore(String trustStore) { this.trustStore = trustStore; } - public String getTrustStorePassphrase() { + public @Nullable String getTrustStorePassphrase() { return this.trustStorePassphrase; } @@ -487,7 +490,7 @@ public void setTrustStoreType(String trustStoreType) { this.trustStoreType = trustStoreType; } - public String getSaslConfig() { + public @Nullable String getSaslConfig() { return this.saslConfig; } @@ -537,7 +540,7 @@ public void setContentType(String contentType) { this.contentType = contentType; } - public String getContentEncoding() { + public @Nullable String getContentEncoding() { return this.contentEncoding; } @@ -545,7 +548,7 @@ public void setContentEncoding(String contentEncoding) { this.contentEncoding = contentEncoding; } - public String getApplicationId() { + public @Nullable String getApplicationId() { return this.applicationId; } @@ -609,7 +612,7 @@ public void setGenerateId(boolean generateId) { this.generateId = generateId; } - public String getCharset() { + public @Nullable String getCharset() { return this.charset; } @@ -621,7 +624,7 @@ public void setLayout(Layout layout) { this.layout = layout; } - public Encoder getEncoder() { + public @Nullable Encoder getEncoder() { return this.encoder; } @@ -666,7 +669,7 @@ public boolean isIncludeCallerData() { /** * If true, the caller data will be available in the target AMQP message. - * By default no caller data is sent to the RabbitMQ. + * By default, no caller data is sent to the RabbitMQ. * @param includeCallerData include or on caller data * @since 1.7.1 * @see ILoggingEvent#getCallerData() @@ -676,14 +679,17 @@ public void setIncludeCallerData(boolean includeCallerData) { } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void start() { this.events = createEventQueue(); ConnectionFactory rabbitConnectionFactory = createRabbitConnectionFactory(); if (rabbitConnectionFactory != null) { super.start(); - this.routingKeyLayout.setPattern(this.routingKeyLayout.getPattern() - .replaceAll("%property\\{applicationId}", this.applicationId)); + if (this.applicationId != null) { + this.routingKeyLayout.setPattern(this.routingKeyLayout.getPattern() + .replaceAll("%property\\{applicationId}", this.applicationId)); + } this.routingKeyLayout.setContext(getContext()); this.routingKeyLayout.start(); this.locationLayout.setContext(getContext()); @@ -700,10 +706,11 @@ public void start() { this.clientConnectionProperties); updateConnectionClientProperties(this.connectionFactory.getRabbitConnectionFactory().getClientProperties()); setUpExchangeDeclaration(); - this.senderPool = Executors.newCachedThreadPool(); + ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < this.senderPoolSize; i++) { - this.senderPool.submit(new EventSender()); + executorService.submit(new EventSender()); } + this.senderPool = executorService; } } @@ -711,7 +718,7 @@ public void start() { * Create the {@link ConnectionFactory}. * @return a {@link ConnectionFactory}. */ - protected ConnectionFactory createRabbitConnectionFactory() { + protected @Nullable ConnectionFactory createRabbitConnectionFactory() { RabbitConnectionFactoryBean factoryBean = new RabbitConnectionFactoryBean(); configureRabbitConnectionFactory(factoryBean); try { @@ -725,7 +732,7 @@ protected ConnectionFactory createRabbitConnectionFactory() { } /** - * Configure the {@link RabbitConnectionFactoryBean}. Sub-classes may override to + * Configure the {@link RabbitConnectionFactoryBean}. Subclasses may override to * customize the configuration of the bean. * @param factoryBean the {@link RabbitConnectionFactoryBean}. */ @@ -799,14 +806,12 @@ protected BlockingQueue createEventQueue() { @Override public void stop() { super.stop(); - if (null != this.senderPool) { + if (this.senderPool != null) { this.senderPool.shutdownNow(); this.senderPool = null; } - if (null != this.connectionFactory) { - this.connectionFactory.destroy(); - this.connectionFactory.onApplicationEvent(new ContextClosedEvent(this.context)); - } + this.connectionFactory.destroy(); + this.connectionFactory.onApplicationEvent(new ContextClosedEvent(this.context)); this.retryTimer.cancel(); this.routingKeyLayout.stop(); } @@ -823,22 +828,12 @@ protected void append(ILoggingEvent event) { protected void setUpExchangeDeclaration() { RabbitAdmin admin = new RabbitAdmin(this.connectionFactory); if (this.declareExchange) { - Exchange x; - if ("topic".equals(this.exchangeType)) { - x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("direct".equals(this.exchangeType)) { - x = new DirectExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("fanout".equals(this.exchangeType)) { - x = new FanoutExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("headers".equals(this.exchangeType)) { - x = new HeadersExchange(this.exchangeType, this.durable, this.autoDelete); - } - else { - x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete); - } + Exchange x = switch (this.exchangeType) { + case "direct" -> new DirectExchange(this.exchangeName, this.durable, this.autoDelete); + case "fanout" -> new FanoutExchange(this.exchangeName, this.durable, this.autoDelete); + case "headers" -> new HeadersExchange(this.exchangeType, this.durable, this.autoDelete); + default -> new TopicExchange(this.exchangeName, this.durable, this.autoDelete); + }; this.connectionFactory.addConnectionListener(new DeclareExchangeConnectionListener(x, admin)); } } @@ -952,10 +947,10 @@ private void sendOneEncoderPatternMessage(RabbitTemplate rabbitTemplate, String private void doSend(RabbitTemplate rabbitTemplate, final Event event, ILoggingEvent logEvent, String name, MessageProperties amqpProps, String routingKey) { byte[] msgBody; - if (AmqpAppender.this.abbreviator != null && logEvent instanceof LoggingEvent) { - ((LoggingEvent) logEvent).setLoggerName(AmqpAppender.this.abbreviator.abbreviate(name)); + if (AmqpAppender.this.abbreviator != null && logEvent instanceof LoggingEvent logEv) { + logEv.setLoggerName(AmqpAppender.this.abbreviator.abbreviate(name)); msgBody = encodeMessage(logEvent); - ((LoggingEvent) logEvent).setLoggerName(name); + logEv.setLoggerName(name); } else { msgBody = encodeMessage(logEvent); @@ -996,6 +991,7 @@ private byte[] encodeMessage(ILoggingEvent logEvent) { return AmqpAppender.this.encoder.encode(logEvent); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation String msgBody = AmqpAppender.this.layout.doLayout(logEvent); if (AmqpAppender.this.charset != null) { try { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java index 6bb676d12f..e63351d0fe 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting Logback appenders. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.logback; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/package-info.java index 9880b9bcef..8fa710bac5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/package-info.java @@ -1,4 +1,5 @@ /** * Provides top-level classes for Spring Rabbit. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/MessageRecoverer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/MessageRecoverer.java index 9c547b9696..61859705df 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/MessageRecoverer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/MessageRecoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -19,6 +19,9 @@ import org.springframework.amqp.core.Message; /** + * Implementations of this interface can handle failed messages after retries are + * exhausted. + * * @author Dave Syer * @author Gary Russell * diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java index 271b5c2d75..d7ed3d3726 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; @@ -29,7 +30,10 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.connection.RabbitUtils; import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.lang.Nullable; +import org.springframework.amqp.rabbit.support.ValueExpression; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; /** @@ -68,9 +72,11 @@ public class RepublishMessageRecoverer implements MessageRecoverer { protected final AmqpTemplate errorTemplate; // NOSONAR - protected final String errorRoutingKey; // NOSONAR + protected final Expression errorRoutingKeyExpression; - protected final String errorExchangeName; // NOSONAR + protected final Expression errorExchangeNameExpression; + + protected final EvaluationContext evaluationContext = new StandardEvaluationContext(); private String errorRoutingKeyPrefix = "error."; @@ -80,19 +86,50 @@ public class RepublishMessageRecoverer implements MessageRecoverer { private MessageDeliveryMode deliveryMode = MessageDeliveryMode.PERSISTENT; + /** + * Create an instance with the provided template. + * @param errorTemplate the template. + */ public RepublishMessageRecoverer(AmqpTemplate errorTemplate) { - this(errorTemplate, null, null); + this(errorTemplate, null, (String) null); } + /** + * Create an instance with the provided properties. + * @param errorTemplate the template. + * @param errorExchange the exchange. + */ public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchange) { this(errorTemplate, errorExchange, null); } - public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchange, String errorRoutingKey) { + /** + * Create an instance with the provided properties. If the exchange or routing key is null, + * the template's default will be used. + * @param errorTemplate the template. + * @param errorExchange the exchange. + * @param errorRoutingKey the routing key. + */ + public RepublishMessageRecoverer(AmqpTemplate errorTemplate, @Nullable String errorExchange, + @Nullable String errorRoutingKey) { + + this(errorTemplate, new ValueExpression<>(errorExchange), new ValueExpression<>(errorRoutingKey)); + } + + /** + * Create an instance with the provided properties. If the exchange or routing key + * evaluate to null, the template's default will be used. + * @param errorTemplate the template. + * @param errorExchange the exchange expression, evaluated against the message. + * @param errorRoutingKey the routing key, evaluated against the message. + */ + public RepublishMessageRecoverer(AmqpTemplate errorTemplate, @Nullable Expression errorExchange, + @Nullable Expression errorRoutingKey) { + Assert.notNull(errorTemplate, "'errorTemplate' cannot be null"); this.errorTemplate = errorTemplate; - this.errorExchangeName = errorExchange; - this.errorRoutingKey = errorRoutingKey; + this.errorExchangeNameExpression = errorExchange != null ? errorExchange : new ValueExpression<>(null); + this.errorRoutingKeyExpression = errorRoutingKey != null ? errorRoutingKey : new ValueExpression<>(null); if (!(this.errorTemplate instanceof RabbitTemplate)) { this.maxStackTraceLength = Integer.MAX_VALUE; } @@ -154,9 +191,9 @@ protected MessageDeliveryMode getDeliveryMode() { @Override public void recover(Message message, Throwable cause) { MessageProperties messageProperties = message.getMessageProperties(); - Map headers = messageProperties.getHeaders(); + Map headers = messageProperties.getHeaders(); String exceptionMessage = cause.getCause() != null ? cause.getCause().getMessage() : cause.getMessage(); - String[] processed = processStackTrace(cause, exceptionMessage); + @Nullable String[] processed = processStackTrace(cause, exceptionMessage); String stackTraceAsString = processed[0]; String truncatedExceptionMessage = processed[1]; if (truncatedExceptionMessage != null) { @@ -175,17 +212,17 @@ public void recover(Message message, Throwable cause) { messageProperties.setDeliveryMode(this.deliveryMode); } - if (null != this.errorExchangeName) { - String routingKey = this.errorRoutingKey != null ? this.errorRoutingKey - : this.prefixedOriginalRoutingKey(message); - doSend(this.errorExchangeName, routingKey, message); + String exchangeName = this.errorExchangeNameExpression.getValue(this.evaluationContext, message, String.class); + String rk = this.errorRoutingKeyExpression.getValue(this.evaluationContext, message, String.class); + String routingKey = rk != null ? rk : this.prefixedOriginalRoutingKey(message); + if (null != exchangeName) { + doSend(exchangeName, routingKey, message); if (this.logger.isWarnEnabled()) { - this.logger.warn("Republishing failed message to exchange '" + this.errorExchangeName + this.logger.warn("Republishing failed message to exchange '" + exchangeName + "' with routing key " + routingKey); } } else { - final String routingKey = this.prefixedOriginalRoutingKey(message); doSend(null, routingKey, message); if (this.logger.isWarnEnabled()) { this.logger.warn("Republishing failed message to the template's default exchange with routing key " @@ -210,7 +247,7 @@ protected void doSend(@Nullable String exchange, String routingKey, Message mess } } - private String[] processStackTrace(Throwable cause, String exceptionMessage) { + private @Nullable String[] processStackTrace(Throwable cause, @Nullable String exceptionMessage) { String stackTraceAsString = getStackTraceAsString(cause); if (this.maxStackTraceLength < 0) { int maxStackTraceLen = RabbitUtils @@ -223,7 +260,7 @@ private String[] processStackTrace(Throwable cause, String exceptionMessage) { return truncateIfNecessary(cause, exceptionMessage, stackTraceAsString); } - private String[] truncateIfNecessary(Throwable cause, String exception, String stackTrace) { + private @Nullable String[] truncateIfNecessary(Throwable cause, @Nullable String exception, String stackTrace) { boolean truncated = false; String stackTraceAsString = stackTrace; String exceptionMessage = exception == null ? "" : exception; @@ -258,7 +295,7 @@ else if (stackTraceAsString.length() + exceptionMessage.length() > this.maxStack } } } - return new String[] { stackTraceAsString, truncated ? truncatedExceptionMessage : null }; + return new @Nullable String[] {stackTraceAsString, truncated ? truncatedExceptionMessage : null}; } /** @@ -267,15 +304,28 @@ else if (stackTraceAsString.length() + exceptionMessage.length() > this.maxStack * @param cause The cause. * @return A {@link Map} of additional headers to add. */ - protected Map additionalHeaders(Message message, Throwable cause) { + protected @Nullable Map additionalHeaders(Message message, Throwable cause) { return null; } - private String prefixedOriginalRoutingKey(Message message) { + /** + * The default behavior of this method is to append the received routing key to the + * {@link #setErrorRoutingKeyPrefix(String) routingKeyPrefix}. This is only invoked + * if the routing key is null. + * @param message the message. + * @return the routing key. + */ + protected String prefixedOriginalRoutingKey(Message message) { return this.errorRoutingKeyPrefix + message.getMessageProperties().getReceivedRoutingKey(); } - private String getStackTraceAsString(Throwable cause) { + /** + * Create a String representation of the stack trace. + * @param cause the throwable. + * @return the String. + * @since 2.4.8 + */ + protected String getStackTraceAsString(Throwable cause) { StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter, true); cause.printStackTrace(printWriter); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java index 1c65f5931e..0f9f79a32e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author 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.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.AmqpMessageReturnedException; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; @@ -28,12 +30,14 @@ import org.springframework.amqp.rabbit.core.AmqpNackReceivedException; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; -import org.springframework.lang.Nullable; +import org.springframework.expression.Expression; /** * A {@link RepublishMessageRecoverer} supporting publisher confirms and returns. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.3.3 * */ @@ -48,20 +52,20 @@ public class RepublishMessageRecovererWithConfirms extends RepublishMessageRecov private long confirmTimeout = DEFAULT_TIMEOUT; /** - * Use the supplied template to publish the messsage with the provided confirm type. + * Use the supplied template to publish the message with the provided confirm type. * The template and its connection factory must be suitably configured to support the - * confirm type. + * {@code confirm} type. * @param errorTemplate the template. * @param confirmType the confirmType. */ public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, ConfirmType confirmType) { - this(errorTemplate, null, null, confirmType); + this(errorTemplate, (Expression) null, null, confirmType); } /** - * Use the supplied template to publish the messsage with the provided confirm type to + * Use the supplied template to publish the message with the provided confirm type to * the provided exchange with the default routing key. The template and its connection - * factory must be suitably configured to support the confirm type. + * factory must be suitably configured to support the {@code confirm} type. * @param errorTemplate the template. * @param confirmType the confirmType. * @param errorExchange the exchange. @@ -73,16 +77,34 @@ public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, Strin } /** - * Use the supplied template to publish the messsage with the provided confirm type to + * Use the supplied template to publish the message with the provided confirm type to * the provided exchange with the provided routing key. The template and its - * connection factory must be suitably configured to support the confirm type. + * connection factory must be suitably configured to support the {@code confirm} type. * @param errorTemplate the template. * @param confirmType the confirmType. * @param errorExchange the exchange. * @param errorRoutingKey the routing key. */ public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, String errorExchange, - String errorRoutingKey, ConfirmType confirmType) { + @Nullable String errorRoutingKey, ConfirmType confirmType) { + + super(errorTemplate, errorExchange, errorRoutingKey); + this.template = errorTemplate; + this.confirmType = confirmType; + } + + /** + * Use the supplied template to publish the message with the provided confirm type to + * the provided exchange with the provided routing key. The template and its + * connection factory must be suitably configured to support the {@code confirm} type. + * @param errorTemplate the template. + * @param confirmType the confirmType. + * @param errorExchange the exchange. + * @param errorRoutingKey the routing key. + * @since 3.1.2 + */ + public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, @Nullable Expression errorExchange, + @Nullable Expression errorRoutingKey, ConfirmType confirmType) { super(errorTemplate, errorExchange, errorRoutingKey); this.template = errorTemplate; @@ -90,7 +112,7 @@ public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, Strin } /** - * Set the confirm timeout; default 10 seconds. + * Set the {@code confirm} timeout; default 10 seconds. * @param confirmTimeout the timeout. */ public void setConfirmTimeout(long confirmTimeout) { @@ -98,8 +120,7 @@ public void setConfirmTimeout(long confirmTimeout) { } @Override - protected void doSend(@Nullable - String exchange, String routingKey, Message message) { + protected void doSend(@Nullable String exchange, String routingKey, Message message) { if (ConfirmType.CORRELATED.equals(this.confirmType)) { doSendCorrelated(exchange, routingKey, message); } @@ -108,7 +129,7 @@ protected void doSend(@Nullable } } - private void doSendCorrelated(String exchange, String routingKey, Message message) { + private void doSendCorrelated(@Nullable String exchange, String routingKey, Message message) { CorrelationData cd = new CorrelationData(); if (exchange != null) { this.template.send(exchange, routingKey, message, cd); @@ -121,7 +142,7 @@ private void doSendCorrelated(String exchange, String routingKey, Message messag if (cd.getReturned() != null) { throw new AmqpMessageReturnedException("Message returned", cd.getReturned()); } - if (!confirm.isAck()) { + if (!confirm.ack()) { throw new AmqpNackReceivedException("Negative acknowledgment received", message); } } @@ -137,7 +158,7 @@ private void doSendCorrelated(String exchange, String routingKey, Message messag } } - private void doSendSimple(String exchange, String routingKey, Message message) { + private void doSendSimple(@Nullable String exchange, String routingKey, Message message) { this.template.invoke(sender -> { if (exchange != null) { sender.send(exchange, routingKey, message); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/package-info.java index 6346485ee4..4948f5de8e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting retries. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.retry; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ActiveObjectCounter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ActiveObjectCounter.java index 30abbced57..e37a5653be 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ActiveObjectCounter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ActiveObjectCounter.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,11 +29,12 @@ * * @author Dave Syer * @author Artem Bilan + * @author Ngoc Nhan * */ public class ActiveObjectCounter { - private final ConcurrentMap locks = new ConcurrentHashMap(); + private final ConcurrentMap locks = new ConcurrentHashMap<>(); private volatile boolean active = true; @@ -56,7 +57,7 @@ public boolean await(long timeout, TimeUnit timeUnit) throws InterruptedExceptio if (this.locks.isEmpty()) { return true; } - Collection objects = new HashSet(this.locks.keySet()); + Collection objects = new HashSet<>(this.locks.keySet()); for (T object : objects) { CountDownLatch lock = this.locks.get(object); if (lock == null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ConsumerCancelledException.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ConsumerCancelledException.java index 1ad1a4c9a6..b5e9b74fcb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ConsumerCancelledException.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ConsumerCancelledException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.support; +import java.io.Serial; + /** * Thrown when the broker cancels the consumer and the message * queue is drained. @@ -26,6 +28,7 @@ */ public class ConsumerCancelledException extends RuntimeException { + @Serial private static final long serialVersionUID = 3815997920289066359L; public ConsumerCancelledException() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index 4fb2d0c73f..728b43e9f9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,22 +24,28 @@ import java.util.List; import java.util.Map; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.LongString; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.LongString; - /** * Default implementation of the {@link MessagePropertiesConverter} strategy. * * @author Mark Fisher * @author Gary Russell * @author Soeren Unruh + * @author Raylax Grey + * @author Artem Bilan + * @author Ngoc Nhan + * @author Johan Kaving + * @author Raul Avila + * * @since 1.0 */ public class DefaultMessagePropertiesConverter implements MessagePropertiesConverter { @@ -83,8 +89,8 @@ public DefaultMessagePropertiesConverter(int longStringLimit, boolean convertLon } @Override - public MessageProperties toMessageProperties(final BasicProperties source, final Envelope envelope, - final String charset) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public MessageProperties toMessageProperties(BasicProperties source, @Nullable Envelope envelope, String charset) { MessageProperties target = new MessageProperties(); Map headers = source.getHeaders(); if (!CollectionUtils.isEmpty(headers)) { @@ -92,8 +98,16 @@ public MessageProperties toMessageProperties(final BasicProperties source, final String key = entry.getKey(); if (MessageProperties.X_DELAY.equals(key)) { Object value = entry.getValue(); - if (value instanceof Integer) { - target.setReceivedDelay((Integer) value); + if (value instanceof Number numberValue) { + long receivedDelayLongValue = Math.abs(numberValue.longValue()); + target.setReceivedDelayLong(receivedDelayLongValue); + target.setHeader(key, receivedDelayLongValue); + } + } + else if (MessageProperties.RETRY_COUNT.equals(key)) { + Object value = entry.getValue(); + if (value instanceof Number numberValue) { + target.setRetryCount(numberValue.longValue()); } } else { @@ -130,27 +144,40 @@ public MessageProperties toMessageProperties(final BasicProperties source, final target.setRedelivered(envelope.isRedeliver()); target.setDeliveryTag(envelope.getDeliveryTag()); } + + if (target.getRetryCount() == 0) { + List> xDeathHeader = target.getXDeathHeader(); + if (!CollectionUtils.isEmpty(xDeathHeader)) { + Object value = xDeathHeader.get(0).get("count"); + + if (value instanceof Number numberValue) { + target.setRetryCount(numberValue.longValue()); + } + } + } + return target; } @Override public BasicProperties fromMessageProperties(final MessageProperties source, final String charset) { BasicProperties.Builder target = new BasicProperties.Builder(); - target.headers(this.convertHeadersIfNecessary(source.getHeaders())) - .timestamp(source.getTimestamp()) - .messageId(source.getMessageId()) - .userId(source.getUserId()) - .appId(source.getAppId()) - .clusterId(source.getClusterId()) - .type(source.getType()); + Map headers = convertHeadersIfNecessary(source); + target.headers(headers) + .timestamp(source.getTimestamp()) + .messageId(source.getMessageId()) + .userId(source.getUserId()) + .appId(source.getAppId()) + .clusterId(source.getClusterId()) + .type(source.getType()); MessageDeliveryMode deliveryMode = source.getDeliveryMode(); if (deliveryMode != null) { target.deliveryMode(MessageDeliveryMode.toInt(deliveryMode)); } target.expiration(source.getExpiration()) - .priority(source.getPriority()) - .contentType(source.getContentType()) - .contentEncoding(source.getContentEncoding()); + .priority(source.getPriority()) + .contentType(source.getContentType()) + .contentEncoding(source.getContentEncoding()); String correlationId = source.getCorrelationId(); if (StringUtils.hasText(correlationId)) { target.correlationId(correlationId); @@ -162,26 +189,31 @@ public BasicProperties fromMessageProperties(final MessageProperties source, fin return target.build(); } - private Map convertHeadersIfNecessary(Map headers) { - if (CollectionUtils.isEmpty(headers)) { - return Collections.emptyMap(); + private Map convertHeadersIfNecessary(MessageProperties source) { + Map headers = source.getHeaders(); + long retryCount = source.getRetryCount(); + + if (headers.isEmpty() && retryCount == 0) { + return Collections.emptyMap(); } - Map writableHeaders = new HashMap(); + Map writableHeaders = new HashMap<>(); for (Map.Entry entry : headers.entrySet()) { - writableHeaders.put(entry.getKey(), this.convertHeaderValueIfNecessary(entry.getValue())); + writableHeaders.put(entry.getKey(), convertHeaderValueIfNecessary(entry.getValue())); + } + if (retryCount > 0) { + writableHeaders.put(MessageProperties.RETRY_COUNT, retryCount); } return writableHeaders; } /** - * Converts a header value to a String if the value type is unsupported by AMQP, also handling values + * Convert a header value to a String if the value type is unsupported by AMQP, also handling values * nested inside Lists or Maps. *

{@code null} values are passed through, although Rabbit client will throw an IllegalArgumentException. * @param valueArg the value. * @return the converted value. */ - @Nullable // NOSONAR complexity - private Object convertHeaderValueIfNecessary(@Nullable Object valueArg) { + private @Nullable Object convertHeaderValueIfNecessary(@Nullable Object valueArg) { Object value = valueArg; boolean valid = (value instanceof String) || (value instanceof byte[]) // NOSONAR boolean complexity || (value instanceof Boolean) || (value instanceof Class) @@ -192,38 +224,37 @@ private Object convertHeaderValueIfNecessary(@Nullable Object valueArg) { if (!valid && value != null) { value = value.toString(); } - else if (value instanceof Object[]) { - Object[] array = (Object[]) value; - Object[] writableArray = new Object[array.length]; + else if (value instanceof Object[] array) { + @Nullable Object[] writableArray = new Object[array.length]; for (int i = 0; i < writableArray.length; i++) { writableArray[i] = convertHeaderValueIfNecessary(array[i]); } value = writableArray; } - else if (value instanceof List) { - List writableList = new ArrayList(((List) value).size()); - for (Object listValue : (List) value) { + else if (value instanceof List values) { + List<@Nullable Object> writableList = new ArrayList<>(values.size()); + for (Object listValue : values) { writableList.add(convertHeaderValueIfNecessary(listValue)); } value = writableList; } else if (value instanceof Map) { @SuppressWarnings("unchecked") - Map originalMap = (Map) value; - Map writableMap = new HashMap(originalMap.size()); + Map originalMap = (Map) value; + Map writableMap = new HashMap<>(originalMap.size()); for (Map.Entry entry : originalMap.entrySet()) { - writableMap.put(entry.getKey(), this.convertHeaderValueIfNecessary(entry.getValue())); + writableMap.put(entry.getKey(), convertHeaderValueIfNecessary(entry.getValue())); } value = writableMap; } - else if (value instanceof Class) { - value = ((Class) value).getName(); + else if (value instanceof Class clazz) { + value = clazz.getName(); } return value; } /** - * Converts a LongString value to either a String or DataInputStream based on a + * Convert a LongString value to either a String or DataInputStream based on a * length-driven threshold. If the length is {@link #longStringLimit} bytes or less, a * String will be returned, otherwise a DataInputStream is returned or the {@link LongString} * is returned unconverted if {@link #convertLongLongStrings} is true. @@ -253,27 +284,24 @@ private Object convertLongString(LongString longString, String charset) { * @return the converted string. */ private Object convertLongStringIfNecessary(Object valueArg, String charset) { - Object value = valueArg; - if (value instanceof LongString) { - value = convertLongString((LongString) value, charset); + if (valueArg instanceof LongString longStr) { + return convertLongString(longStr, charset); } - else if (value instanceof List) { - List convertedList = new ArrayList(((List) value).size()); - for (Object listValue : (List) value) { - convertedList.add(this.convertLongStringIfNecessary(listValue, charset)); - } - value = convertedList; + + if (valueArg instanceof List values) { + List convertedList = new ArrayList<>(values.size()); + values.forEach(value -> convertedList.add(this.convertLongStringIfNecessary(value, charset))); + return convertedList; } - else if (value instanceof Map) { - @SuppressWarnings("unchecked") - Map originalMap = (Map) value; - Map convertedMap = new HashMap(); - for (Map.Entry entry : originalMap.entrySet()) { - convertedMap.put(entry.getKey(), this.convertLongStringIfNecessary(entry.getValue(), charset)); - } - value = convertedMap; + + if (valueArg instanceof Map originalMap) { + Map convertedMap = new HashMap<>(); + originalMap.forEach( + (key, value) -> convertedMap.put((String) key, this.convertLongStringIfNecessary(value, charset))); + return convertedMap; } - return value; + + return valueArg; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerContainerAware.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerContainerAware.java index bfee888a4e..a3711aed7f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerContainerAware.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerContainerAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.util.Collection; +import org.jspecify.annotations.Nullable; + /** * {@link org.springframework.amqp.core.MessageListener}s that also implement this * interface can have configuration verified during initialization. @@ -34,6 +36,7 @@ public interface ListenerContainerAware { * * @return the queue names. */ + @Nullable Collection expectedQueueNames(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerExecutionFailedException.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerExecutionFailedException.java index 40bd576a3a..a74c07d171 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerExecutionFailedException.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerExecutionFailedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; @@ -31,6 +33,7 @@ * * @author Juergen Hoeller * @author Gary Russell + * @author Artem Bilan * * @see org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter */ @@ -50,8 +53,8 @@ public ListenerExecutionFailedException(String msg, Throwable cause, Message... this.failedMessages.addAll(Arrays.asList(failedMessage)); } - public Message getFailedMessage() { - return this.failedMessages.get(0); + public @Nullable Message getFailedMessage() { + return this.failedMessages.isEmpty() ? null : this.failedMessages.get(0); } public Collection getFailedMessages() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java index 8c91016f05..aa26e13046 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package org.springframework.amqp.rabbit.support; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; - import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Envelope; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.MessageProperties; /** * Strategy interface for converting between Spring AMQP {@link MessageProperties} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java index 19e339bb83..0ff98fed13 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,11 @@ import java.net.ConnectException; import java.util.concurrent.TimeoutException; +import com.rabbitmq.client.ConsumerCancelledException; +import com.rabbitmq.client.PossibleAuthenticationFailureException; +import com.rabbitmq.client.ShutdownSignalException; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpException; @@ -28,11 +33,6 @@ import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.AmqpUnsupportedEncodingException; import org.springframework.amqp.UncategorizedAmqpException; -import org.springframework.util.Assert; - -import com.rabbitmq.client.ConsumerCancelledException; -import com.rabbitmq.client.PossibleAuthenticationFailureException; -import com.rabbitmq.client.ShutdownSignalException; /** * Translates Rabbit Exceptions to the {@link AmqpException} class @@ -49,16 +49,15 @@ public final class RabbitExceptionTranslator { private RabbitExceptionTranslator() { } - public static RuntimeException convertRabbitAccessException(Throwable ex) { - Assert.notNull(ex, "Exception must not be null"); - if (ex instanceof AmqpException) { - return (AmqpException) ex; + public static RuntimeException convertRabbitAccessException(@Nullable Throwable ex) { + if (ex instanceof AmqpException amqpException) { + return amqpException; } - if (ex instanceof ShutdownSignalException) { - return new AmqpConnectException((ShutdownSignalException) ex); + if (ex instanceof ShutdownSignalException sigEx) { + return new AmqpConnectException(sigEx); } - if (ex instanceof ConnectException) { - return new AmqpConnectException((ConnectException) ex); + if (ex instanceof ConnectException connEx) { + return new AmqpConnectException(connEx); } if (ex instanceof PossibleAuthenticationFailureException) { return new AmqpAuthenticationException(ex); @@ -66,8 +65,8 @@ public static RuntimeException convertRabbitAccessException(Throwable ex) { if (ex instanceof UnsupportedEncodingException) { return new AmqpUnsupportedEncodingException(ex); } - if (ex instanceof IOException) { - return new AmqpIOException((IOException) ex); + if (ex instanceof IOException ioEx) { + return new AmqpIOException(ioEx); } if (ex instanceof TimeoutException) { return new AmqpTimeoutException(ex); @@ -75,8 +74,8 @@ public static RuntimeException convertRabbitAccessException(Throwable ex) { if (ex instanceof ConsumerCancelledException) { return new org.springframework.amqp.rabbit.support.ConsumerCancelledException(ex); } - if (ex instanceof org.springframework.amqp.rabbit.support.ConsumerCancelledException) { - return (org.springframework.amqp.rabbit.support.ConsumerCancelledException) ex; + if (ex instanceof org.springframework.amqp.rabbit.support.ConsumerCancelledException consumerCancelledException) { + return consumerCancelledException; } // fallback return new UncategorizedAmqpException(ex); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ValueExpression.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ValueExpression.java index 6b228a2c57..a4dbda0dd0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ValueExpression.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ValueExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,124 +16,133 @@ package org.springframework.amqp.rabbit.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.TypedValue; -import org.springframework.util.Assert; +import org.springframework.expression.common.ExpressionUtils; /** - * A very simple hardcoded implementation of the {@link org.springframework.expression.Expression} + * A very simple hardcoded implementation of the {@link Expression} * interface that represents an immutable value. * It is used as value holder in the context of expression evaluation. * * @param - The expected value type. * * @author Artem Bilan + * * @since 1.4 */ public class ValueExpression implements Expression { /** Fixed value of this expression. */ - private final V value; + private final @Nullable V value; - private final Class aClass; + private final @Nullable Class aClass; private final TypedValue typedResultValue; - private final TypeDescriptor typeDescriptor; + private final @Nullable TypeDescriptor typeDescriptor; @SuppressWarnings("unchecked") - public ValueExpression(V value) { - Assert.notNull(value, "'value' must not be null"); + public ValueExpression(@Nullable V value) { this.value = value; - this.aClass = (Class) this.value.getClass(); + this.aClass = (Class) (this.value != null ? this.value.getClass() : null); this.typedResultValue = new TypedValue(this.value); this.typeDescriptor = this.typedResultValue.getTypeDescriptor(); } @Override - public V getValue() throws EvaluationException { + public @Nullable V getValue() throws EvaluationException { return this.value; } @Override - public V getValue(Object rootObject) throws EvaluationException { + public @Nullable V getValue(@Nullable Object rootObject) throws EvaluationException { return this.value; } @Override - public V getValue(EvaluationContext context) throws EvaluationException { + public @Nullable V getValue(EvaluationContext context) throws EvaluationException { return this.value; } @Override - public V getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + public @Nullable V getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { return this.value; } @Override - public T getValue(Object rootObject, Class desiredResultType) throws EvaluationException { + public @Nullable T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) + throws EvaluationException { + return getValue(desiredResultType); } @Override - public T getValue(Class desiredResultType) throws EvaluationException { - return org.springframework.expression.common.ExpressionUtils - .convertTypedValue(null, this.typedResultValue, desiredResultType); + public @Nullable T getValue(@Nullable Class desiredResultType) throws EvaluationException { + return ExpressionUtils.convertTypedValue(null, this.typedResultValue, desiredResultType); } @Override - public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) + public @Nullable T getValue(EvaluationContext context, @Nullable Object rootObject, + @Nullable Class desiredResultType) throws EvaluationException { + return getValue(context, desiredResultType); } @Override - public T getValue(EvaluationContext context, Class desiredResultType) throws EvaluationException { - return org.springframework.expression.common.ExpressionUtils - .convertTypedValue(context, this.typedResultValue, desiredResultType); + public @Nullable T getValue(EvaluationContext context, @Nullable Class desiredResultType) + throws EvaluationException { + + return ExpressionUtils.convertTypedValue(context, this.typedResultValue, desiredResultType); } @Override - public Class getValueType() throws EvaluationException { + public @Nullable Class getValueType() throws EvaluationException { return this.aClass; } @Override - public Class getValueType(Object rootObject) throws EvaluationException { + public @Nullable Class getValueType(@Nullable Object rootObject) throws EvaluationException { return this.aClass; } @Override - public Class getValueType(EvaluationContext context) throws EvaluationException { + public @Nullable Class getValueType(EvaluationContext context) throws EvaluationException { return this.aClass; } @Override - public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { + public @Nullable Class getValueType(EvaluationContext context, @Nullable Object rootObject) + throws EvaluationException { + return this.aClass; } @Override - public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { + public @Nullable TypeDescriptor getValueTypeDescriptor() throws EvaluationException { return this.typeDescriptor; } @Override - public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { + public @Nullable TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException { return this.typeDescriptor; } @Override - public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { + public @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { return this.typeDescriptor; } @Override - public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) + public @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + return this.typeDescriptor; } @@ -143,33 +152,35 @@ public boolean isWritable(EvaluationContext context) throws EvaluationException } @Override - public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { + public boolean isWritable(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { return false; } @Override - public boolean isWritable(Object rootObject) throws EvaluationException { + public boolean isWritable(@Nullable Object rootObject) throws EvaluationException { return false; } @Override - public void setValue(EvaluationContext context, Object value) throws EvaluationException { + public void setValue(EvaluationContext context, @Nullable Object value) throws EvaluationException { setValue(context, null, value); } @Override - public void setValue(Object rootObject, Object value) throws EvaluationException { + public void setValue(@Nullable Object rootObject, @Nullable Object value) throws EvaluationException { setValue(null, rootObject, value); } @Override - public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { - throw new EvaluationException(this.value.toString(), "Cannot call setValue() on a ValueExpression"); + public void setValue(@Nullable EvaluationContext context, @Nullable Object rootObject, @Nullable Object value) + throws EvaluationException { + + throw new EvaluationException(getExpressionString(), "Cannot call setValue() on a ValueExpression"); } @Override public String getExpressionString() { - return this.value.toString(); + return this.value != null ? this.value.toString() : "null"; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java new file mode 100644 index 0000000000..d575a07b70 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java @@ -0,0 +1,148 @@ +/* + * Copyright 2022-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * Spring Rabbit Observation for listeners. + * + * @author Gary Russell + * @author Vincent Meunier + * @author Artem Bilan + * @author Ngoc Nhan + * + * @since 3.0 + */ +public enum RabbitListenerObservation implements ObservationDocumentation { + + /** + * Observation for Rabbit listeners. + */ + LISTENER_OBSERVATION { + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitListenerObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ListenerLowCardinalityTags.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return ListenerHighCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum ListenerLowCardinalityTags implements KeyName { + + /** + * Listener id. + */ + LISTENER_ID { + + @Override + public String asString() { + return "spring.rabbit.listener.id"; + } + + }, + + /** + * The queue the listener is plugged to. + * + * @since 3.2 + */ + DESTINATION_NAME { + + @Override + public String asString() { + return "messaging.destination.name"; + } + + } + + } + + /** + * High cardinality tags. + * + * @since 3.2.1 + */ + public enum ListenerHighCardinalityTags implements KeyName { + + /** + * The delivery tag. + */ + DELIVERY_TAG { + + @Override + public String asString() { + return "messaging.rabbitmq.message.delivery_tag"; + } + + } + + } + + + /** + * Default {@link RabbitListenerObservationConvention} for Rabbit listener key values. + */ + public static class DefaultRabbitListenerObservationConvention implements RabbitListenerObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitListenerObservationConvention INSTANCE = + new DefaultRabbitListenerObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { + final var messageProperties = context.getCarrier().getMessageProperties(); + return KeyValues.of( + RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), + context.getListenerId(), + RabbitListenerObservation.ListenerLowCardinalityTags.DESTINATION_NAME.asString(), + messageProperties.getConsumerQueue()); + } + + @Override + public KeyValues getHighCardinalityKeyValues(RabbitMessageReceiverContext context) { + return KeyValues.of(RabbitListenerObservation.ListenerHighCardinalityTags.DELIVERY_TAG.asString(), + String.valueOf(context.getCarrier().getMessageProperties().getDeliveryTag())); + } + + @Override + public String getContextualName(RabbitMessageReceiverContext context) { + return context.getSource() + " receive"; + } + + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java new file mode 100644 index 0000000000..bbf1f27df5 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java @@ -0,0 +1,41 @@ +/* + * Copyright 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.amqp.rabbit.support.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit listener key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface RabbitListenerObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitMessageReceiverContext; + } + + @Override + default String getName() { + return "spring.rabbit.listener"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java new file mode 100644 index 0000000000..52abc4f4d5 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.observation.transport.ReceiverContext; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Message; + +/** + * {@link ReceiverContext} for {@link Message}s. + * + * @author Gary Russell + * @author Artem Bilan + * + * @since 3.0 + * + */ +public class RabbitMessageReceiverContext extends ReceiverContext { + + private final String listenerId; + + private final Message message; + + @SuppressWarnings("this-escape") + public RabbitMessageReceiverContext(Message message, String listenerId) { + super((carrier, key) -> carrier.getMessageProperties().getHeader(key)); + setCarrier(message); + this.message = message; + this.listenerId = listenerId; + setRemoteServiceName("RabbitMQ"); + } + + public String getListenerId() { + return this.listenerId; + } + + /** + * Return the source (queue) for this message. + * @return the source. + */ + public @Nullable String getSource() { + return this.message.getMessageProperties().getConsumerQueue(); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java new file mode 100644 index 0000000000..dfd1d6d216 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java @@ -0,0 +1,90 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.observation.transport.SenderContext; + +import org.springframework.amqp.core.Message; + +/** + * {@link SenderContext} for {@link Message}s. + * + * @author Gary Russell + * @author Ngoc Nhan + * @since 3.0 + * + */ +public class RabbitMessageSenderContext extends SenderContext { + + private final String beanName; + + private final String destination; + + private final String exchange; + + private final String routingKey; + + /** + * Create an instance {@code RabbitMessageSenderContext}. + * @param message a message to send + * @param beanName the bean name + * @param exchange the name of the exchange + * @param routingKey the routing key + * @since 3.2 + */ + @SuppressWarnings("this-escape") + public RabbitMessageSenderContext(Message message, String beanName, String exchange, String routingKey) { + super((carrier, key, value) -> message.getMessageProperties().setHeader(key, value)); + setCarrier(message); + this.beanName = beanName; + this.exchange = exchange; + this.routingKey = routingKey; + this.destination = exchange + "/" + routingKey; + setRemoteServiceName("RabbitMQ"); + } + + public String getBeanName() { + return this.beanName; + } + + /** + * Return the destination - {@code exchange/routingKey}. + * @return the destination. + */ + public String getDestination() { + return this.destination; + } + + /** + * Return the exchange. + * @return the exchange. + * @since 3.2 + */ + public String getExchange() { + return this.exchange; + } + + /** + * Return the routingKey. + * @return the routingKey. + * @since 3.2 + */ + public String getRoutingKey() { + return this.routingKey; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java new file mode 100644 index 0000000000..06a5551878 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java @@ -0,0 +1,127 @@ +/* + * Copyright 2022-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * Spring RabbitMQ Observation for {@link org.springframework.amqp.rabbit.core.RabbitTemplate}. + * + * @author Gary Russell + * @author Vincent Meunier + * @author Artem Bilan + * + * @since 3.0 + * + */ +public enum RabbitTemplateObservation implements ObservationDocumentation { + + /** + * Observation for RabbitTemplates. + */ + TEMPLATE_OBSERVATION { + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitTemplateObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return TemplateLowCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum TemplateLowCardinalityTags implements KeyName { + + /** + * Bean name of the template. + */ + BEAN_NAME { + + @Override + public String asString() { + return "spring.rabbit.template.name"; + } + + }, + + /** + * The destination exchange (empty if default exchange). + * @since 3.2 + */ + EXCHANGE { + + @Override + public String asString() { + return "messaging.destination.name"; + } + + }, + + /** + * The destination routing key. + * @since 3.2 + */ + ROUTING_KEY { + + @Override + public String asString() { + return "messaging.rabbitmq.destination.routing_key"; + } + + } + + + } + + /** + * Default {@link RabbitTemplateObservationConvention} for Rabbit template key values. + */ + public static class DefaultRabbitTemplateObservationConvention implements RabbitTemplateObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitTemplateObservationConvention INSTANCE = + new DefaultRabbitTemplateObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { + return KeyValues.of( + TemplateLowCardinalityTags.BEAN_NAME.asString(), context.getBeanName(), + TemplateLowCardinalityTags.EXCHANGE.asString(), context.getExchange(), + TemplateLowCardinalityTags.ROUTING_KEY.asString(), context.getRoutingKey() + ); + } + + @Override + public String getContextualName(RabbitMessageSenderContext context) { + return context.getDestination() + " send"; + } + + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java new file mode 100644 index 0000000000..2128d3dd9b --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java @@ -0,0 +1,41 @@ +/* + * Copyright 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.amqp.rabbit.support.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit template key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface RabbitTemplateObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitMessageSenderContext; + } + + @Override + default String getName() { + return "spring.rabbit.template"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java new file mode 100644 index 0000000000..07a19f163e --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides classes for Micrometer support. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.rabbit.support.micrometer; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/package-info.java index 59f6f0ca85..0a94360493 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/package-info.java @@ -1,5 +1,5 @@ /** * Provides support classes for Spring Rabbit. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.support; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.java index 5ff76df564..6da04d3efd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,19 @@ package org.springframework.amqp.rabbit.transaction; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils; import org.springframework.amqp.rabbit.connection.RabbitResourceHolder; +import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.InitializingBean; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; @@ -32,7 +38,7 @@ import org.springframework.util.Assert; /** - * {@link org.springframework.transaction.PlatformTransactionManager} implementation for a single Rabbit + * {@link PlatformTransactionManager} implementation for a single Rabbit * {@link ConnectionFactory}. Binds a Rabbit Channel from the specified ConnectionFactory to the thread, potentially * allowing for one thread-bound channel per ConnectionFactory. * @@ -44,13 +50,13 @@ *

* Application code is required to retrieve the transactional Rabbit resources via * {@link ConnectionFactoryUtils#getTransactionalResourceHolder(ConnectionFactory, boolean)} instead of a standard - * {@link org.springframework.amqp.rabbit.connection.Connection#createChannel(boolean)} call with subsequent + * {@link Connection#createChannel(boolean)} call with subsequent * Channel creation. Spring's - * {@link org.springframework.amqp.rabbit.core.RabbitTemplate} will + * {@link RabbitTemplate} will * autodetect a thread-bound Channel and automatically participate in it. * *

- * The use of {@link org.springframework.amqp.rabbit.connection.CachingConnectionFactory} + * The use of {@link CachingConnectionFactory} * as a target for this transaction manager is strongly recommended. * CachingConnectionFactory uses a single Rabbit Connection for all Rabbit access in order to avoid the overhead of * repeated Connection creation, as well as maintaining a cache of Channels. Each transaction will then share the same @@ -62,12 +68,13 @@ * which has stronger needs for synchronization. * * @author Dave Syer + * @author Artem Bilan */ @SuppressWarnings("serial") public class RabbitTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean { - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; /** * Create a new RabbitTransactionManager for bean-style usage. @@ -81,6 +88,7 @@ public class RabbitTransactionManager extends AbstractPlatformTransactionManager * @see #setConnectionFactory * @see #setTransactionSynchronization */ + @SuppressWarnings("this-escape") public RabbitTransactionManager() { setTransactionSynchronization(SYNCHRONIZATION_NEVER); } @@ -104,7 +112,7 @@ public void setConnectionFactory(ConnectionFactory connectionFactory) { /** * @return the connectionFactory */ - public ConnectionFactory getConnectionFactory() { + public @Nullable ConnectionFactory getConnectionFactory() { return this.connectionFactory; } @@ -118,14 +126,16 @@ public void afterPropertiesSet() { @Override public Object getResourceFactory() { - return getConnectionFactory(); + ConnectionFactory resourceFactory = getConnectionFactory(); + Assert.notNull(resourceFactory, "'connectionFactory' cannot be null"); + return resourceFactory; } @Override protected Object doGetTransaction() { RabbitTransactionObject txObject = new RabbitTransactionObject(); txObject.setResourceHolder((RabbitResourceHolder) TransactionSynchronizationManager - .getResource(getConnectionFactory())); + .getResource(getResourceFactory())); return txObject; } @@ -143,18 +153,20 @@ protected void doBegin(Object transaction, TransactionDefinition definition) { RabbitTransactionObject txObject = (RabbitTransactionObject) transaction; RabbitResourceHolder resourceHolder = null; try { - resourceHolder = ConnectionFactoryUtils.getTransactionalResourceHolder(getConnectionFactory(), true); + ConnectionFactory connectionFactoryToUse = getConnectionFactory(); + Assert.notNull(connectionFactoryToUse, "'connectionFactory' cannot be null"); + resourceHolder = ConnectionFactoryUtils.getTransactionalResourceHolder(connectionFactoryToUse, true); if (logger.isDebugEnabled()) { logger.debug("Created AMQP transaction on channel [" + resourceHolder.getChannel() + "]"); } // resourceHolder.declareTransactional(); txObject.setResourceHolder(resourceHolder); - txObject.getResourceHolder().setSynchronizedWithTransaction(true); + resourceHolder.setSynchronizedWithTransaction(true); int timeout = determineTimeout(definition); if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { - txObject.getResourceHolder().setTimeoutInSeconds(timeout); + resourceHolder.setTimeoutInSeconds(timeout); } - TransactionSynchronizationManager.bindResource(getConnectionFactory(), txObject.getResourceHolder()); + TransactionSynchronizationManager.bindResource(connectionFactoryToUse, resourceHolder); } catch (AmqpException ex) { if (resourceHolder != null) { @@ -168,41 +180,51 @@ protected void doBegin(Object transaction, TransactionDefinition definition) { protected Object doSuspend(Object transaction) { RabbitTransactionObject txObject = (RabbitTransactionObject) transaction; txObject.setResourceHolder(null); - return TransactionSynchronizationManager.unbindResource(getConnectionFactory()); + return TransactionSynchronizationManager.unbindResource(getResourceFactory()); } @Override - protected void doResume(Object transaction, Object suspendedResources) { + protected void doResume(@Nullable Object transaction, Object suspendedResources) { RabbitResourceHolder conHolder = (RabbitResourceHolder) suspendedResources; - TransactionSynchronizationManager.bindResource(getConnectionFactory(), conHolder); + TransactionSynchronizationManager.bindResource(getResourceFactory(), conHolder); } @Override protected void doCommit(DefaultTransactionStatus status) { RabbitTransactionObject txObject = (RabbitTransactionObject) status.getTransaction(); RabbitResourceHolder resourceHolder = txObject.getResourceHolder(); - resourceHolder.commitAll(); + if (resourceHolder != null) { + resourceHolder.commitAll(); + } } @Override protected void doRollback(DefaultTransactionStatus status) { RabbitTransactionObject txObject = (RabbitTransactionObject) status.getTransaction(); RabbitResourceHolder resourceHolder = txObject.getResourceHolder(); - resourceHolder.rollbackAll(); + if (resourceHolder != null) { + resourceHolder.rollbackAll(); + } } @Override protected void doSetRollbackOnly(DefaultTransactionStatus status) { RabbitTransactionObject txObject = (RabbitTransactionObject) status.getTransaction(); - txObject.getResourceHolder().setRollbackOnly(); + RabbitResourceHolder resourceHolder = txObject.getResourceHolder(); + if (resourceHolder != null) { + resourceHolder.setRollbackOnly(); + } } @Override protected void doCleanupAfterCompletion(Object transaction) { RabbitTransactionObject txObject = (RabbitTransactionObject) transaction; - TransactionSynchronizationManager.unbindResource(getConnectionFactory()); - txObject.getResourceHolder().closeAll(); - txObject.getResourceHolder().clear(); + TransactionSynchronizationManager.unbindResource(getResourceFactory()); + RabbitResourceHolder resourceHolder = txObject.getResourceHolder(); + if (resourceHolder != null) { + resourceHolder.closeAll(); + resourceHolder.clear(); + } } /** @@ -212,27 +234,29 @@ protected void doCleanupAfterCompletion(Object transaction) { */ private static class RabbitTransactionObject implements SmartTransactionObject { - private RabbitResourceHolder resourceHolder; + private @Nullable RabbitResourceHolder resourceHolder; RabbitTransactionObject() { } - public void setResourceHolder(RabbitResourceHolder resourceHolder) { + public void setResourceHolder(@Nullable RabbitResourceHolder resourceHolder) { this.resourceHolder = resourceHolder; } - public RabbitResourceHolder getResourceHolder() { + public @Nullable RabbitResourceHolder getResourceHolder() { return this.resourceHolder; } @Override public boolean isRollbackOnly() { - return this.resourceHolder.isRollbackOnly(); + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); } @Override public void flush() { // no-op } + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/package-info.java index 28c0c93c70..167073bec6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting transactions in Spring Rabbit. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.transaction; diff --git a/spring-rabbit/src/main/resources/META-INF/spring/aot.factories b/spring-rabbit/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 0000000000..eb3d20bd46 --- /dev/null +++ b/spring-rabbit/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.amqp.rabbit.aot.RabbitRuntimeHints diff --git a/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd b/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd index 09bdec3bbb..2414c7779f 100644 --- a/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd +++ b/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd @@ -1,8 +1,9 @@ + xmlns:tool="http://www.springframework.org/schema/tool" + xmlns:beans="http://www.springframework.org/schema/beans" + targetNamespace="http://www.springframework.org/schema/rabbit" + elementFormDefault="qualified" attributeFormDefault="unqualified"> @@ -1285,7 +1286,7 @@ @@ -1299,20 +1300,21 @@ A SpEL expression to be evaluated against each request message to determine a 'mandatory' boolean value. The BeanFactoryResolver is available too, if the RabbitTemplate is used from Spring Context, allowing for expressions such as '@myBean.isMandatory(#root)`. - Only applies if a 'return-callback' is provided. Mutually exclusive with 'mandatory'. + Only applies if a 'returns-callback' is provided. Mutually exclusive with 'mandatory'. ]]> - + - + @@ -1326,7 +1328,7 @@ ]]> - + diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java index 75eddf5016..a3db5738b4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or 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,22 @@ package org.springframework.amqp.rabbit; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; - +import java.time.Duration; import java.util.Map; import java.util.UUID; import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Address; @@ -38,33 +41,38 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.Queue; -import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitConverterFuture; -import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitMessageFuture; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.rabbit.listener.adapter.ReplyingMessageListener; +import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.amqp.support.postprocessor.GUnzipPostProcessor; import org.springframework.amqp.support.postprocessor.GZipPostProcessor; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; /** * @author Gary Russell * @author Artem Bilan + * @author Ben Efrati * * @since 1.6 */ @@ -87,10 +95,15 @@ public class AsyncRabbitTemplateTests { private final Message fooMessage = new SimpleMessageConverter().toMessage("foo", new MessageProperties()); + @BeforeAll + static void setup() { + Awaitility.setDefaultTimeout(Duration.ofSeconds(30)); + } + @Test - public void testConvert1Arg() throws Exception { + public void testConvert1Arg() { final AtomicBoolean mppCalled = new AtomicBoolean(); - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("foo", m -> { + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("foo", m -> { mppCalled.set(true); return m; }); @@ -99,10 +112,10 @@ public void testConvert1Arg() throws Exception { } @Test - public void testConvert1ArgDirect() throws Exception { + public void testConvert1ArgDirect() { this.latch.set(new CountDownLatch(1)); - ListenableFuture future1 = this.asyncDirectTemplate.convertSendAndReceive("foo"); - ListenableFuture future2 = this.asyncDirectTemplate.convertSendAndReceive("bar"); + CompletableFuture future1 = this.asyncDirectTemplate.convertSendAndReceive("foo"); + CompletableFuture future2 = this.asyncDirectTemplate.convertSendAndReceive("bar"); this.latch.get().countDown(); checkConverterResult(future1, "FOO"); checkConverterResult(future2, "BAR"); @@ -110,7 +123,8 @@ public void testConvert1ArgDirect() throws Exception { waitForZeroInUseConsumers(); assertThat(TestUtils .getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount", - Integer.class)).isEqualTo(2); + AtomicInteger.class).get()) + .isEqualTo(2); final String missingQueue = UUID.randomUUID().toString(); this.asyncDirectTemplate.convertSendAndReceive("", missingQueue, "foo"); // send to nowhere this.asyncDirectTemplate.stop(); // should clear the inUse channel map @@ -126,20 +140,20 @@ public void testConvert1ArgDirect() throws Exception { } @Test - public void testConvert2Args() throws Exception { - ListenableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName(), "foo"); + public void testConvert2Args() { + CompletableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName(), "foo"); checkConverterResult(future, "FOO"); } @Test - public void testConvert3Args() throws Exception { - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo"); + public void testConvert3Args() { + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo"); checkConverterResult(future, "FOO"); } @Test - public void testConvert4Args() throws Exception { - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo", + public void testConvert4Args() { + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo", message -> { String body = new String(message.getBody()); return new Message((body + "bar").getBytes(), message.getMessageProperties()); @@ -149,15 +163,15 @@ public void testConvert4Args() throws Exception { @Test public void testMessage1Arg() throws Exception { - ListenableFuture future = this.asyncTemplate.sendAndReceive(getFooMessage()); + CompletableFuture future = this.asyncTemplate.sendAndReceive(getFooMessage()); checkMessageResult(future, "FOO"); } @Test public void testMessage1ArgDirect() throws Exception { this.latch.set(new CountDownLatch(1)); - ListenableFuture future1 = this.asyncDirectTemplate.sendAndReceive(getFooMessage()); - ListenableFuture future2 = this.asyncDirectTemplate.sendAndReceive(getFooMessage()); + CompletableFuture future1 = this.asyncDirectTemplate.sendAndReceive(getFooMessage()); + CompletableFuture future2 = this.asyncDirectTemplate.sendAndReceive(getFooMessage()); this.latch.get().countDown(); Message reply1 = checkMessageResult(future1, "FOO"); assertThat(reply1.getMessageProperties().getConsumerQueue()).isEqualTo(Address.AMQ_RABBITMQ_REPLY_TO); @@ -167,29 +181,31 @@ public void testMessage1ArgDirect() throws Exception { waitForZeroInUseConsumers(); assertThat(TestUtils .getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount", - Integer.class)).isEqualTo(2); + AtomicInteger.class).get()) + .isEqualTo(2); this.asyncDirectTemplate.stop(); this.asyncDirectTemplate.start(); assertThat(TestUtils .getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount", - Integer.class)).isEqualTo(0); + AtomicInteger.class).get()) + .isZero(); } - private void waitForZeroInUseConsumers() throws InterruptedException { + private void waitForZeroInUseConsumers() { Map inUseConsumers = TestUtils .getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.inUseConsumerChannels", Map.class); - await().until(() -> inUseConsumers.size() == 0); + await().until(inUseConsumers::isEmpty); } @Test public void testMessage2Args() throws Exception { - ListenableFuture future = this.asyncTemplate.sendAndReceive(this.requests.getName(), getFooMessage()); + CompletableFuture future = this.asyncTemplate.sendAndReceive(this.requests.getName(), getFooMessage()); checkMessageResult(future, "FOO"); } @Test public void testMessage3Args() throws Exception { - ListenableFuture future = this.asyncTemplate.sendAndReceive("", this.requests.getName(), + CompletableFuture future = this.asyncTemplate.sendAndReceive("", this.requests.getName(), getFooMessage()); checkMessageResult(future, "FOO"); } @@ -197,16 +213,16 @@ public void testMessage3Args() throws Exception { @SuppressWarnings("unchecked") @Test public void testCancel() { - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("foo"); + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("foo"); future.cancel(false); - assertThat(TestUtils.getPropertyValue(asyncTemplate, "pending", Map.class)).hasSize(0); + assertThat(TestUtils.getPropertyValue(asyncTemplate, "pending", Map.class)).isEmpty(); } @Test public void testMessageCustomCorrelation() throws Exception { Message message = getFooMessage(); message.getMessageProperties().setCorrelationId("foo"); - ListenableFuture future = this.asyncTemplate.sendAndReceive(message); + CompletableFuture future = this.asyncTemplate.sendAndReceive(message); Message result = checkMessageResult(future, "FOO"); assertThat(result.getMessageProperties().getCorrelationId()).isEqualTo("foo"); } @@ -219,44 +235,46 @@ private Message getFooMessage() { @Test @DirtiesContext - public void testReturn() throws Exception { + public void testReturn() { this.asyncTemplate.setMandatory(true); - ListenableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName() + "x", + CompletableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName() + "x", "foo"); - try { - future.get(10, TimeUnit.SECONDS); - fail("Expected exception"); - } - catch (ExecutionException e) { - assertThat(e.getCause()).isInstanceOf(AmqpMessageReturnedException.class); - assertThat(((AmqpMessageReturnedException) e.getCause()).getRoutingKey()).isEqualTo(this.requests.getName() + "x"); - } + assertThat(future) + .as("Expected exception") + .failsWithin(Duration.ofSeconds(10)) + .withThrowableOfType(ExecutionException.class) + .havingCause() + .isInstanceOf(AmqpMessageReturnedException.class) + .extracting(cause -> ((AmqpMessageReturnedException) cause).getRoutingKey()) + .isEqualTo(this.requests.getName() + "x"); } @Test @DirtiesContext - public void testReturnDirect() throws Exception { + public void testReturnDirect() { this.asyncDirectTemplate.setMandatory(true); - ListenableFuture future = this.asyncDirectTemplate.convertSendAndReceive(this.requests.getName() + "x", + CompletableFuture future = this.asyncDirectTemplate.convertSendAndReceive(this.requests.getName() + "x", "foo"); - try { - future.get(10, TimeUnit.SECONDS); - fail("Expected exception"); - } - catch (ExecutionException e) { - assertThat(e.getCause()).isInstanceOf(AmqpMessageReturnedException.class); - assertThat(((AmqpMessageReturnedException) e.getCause()).getRoutingKey()).isEqualTo(this.requests.getName() + "x"); - } + + assertThat(future) + .as("Expected exception") + .failsWithin(Duration.ofSeconds(10)) + .withThrowableOfType(ExecutionException.class) + .havingCause() + .isInstanceOf(AmqpMessageReturnedException.class) + .extracting(cause -> ((AmqpMessageReturnedException) cause).getRoutingKey()) + .isEqualTo(this.requests.getName() + "x"); } @Test @DirtiesContext - public void testConvertWithConfirm() throws Exception { + public void testConvertWithConfirm() { this.asyncTemplate.setEnableConfirms(true); RabbitConverterFuture future = this.asyncTemplate.convertSendAndReceive("sleep"); - ListenableFuture confirm = future.getConfirm(); - assertThat(confirm).isNotNull(); - assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue(); + CompletableFuture confirm = future.getConfirm(); + assertThat(confirm).isNotNull() + .succeedsWithin(Duration.ofSeconds(10)) + .isEqualTo(true); checkConverterResult(future, "SLEEP"); } @@ -266,20 +284,22 @@ public void testMessageWithConfirm() throws Exception { this.asyncTemplate.setEnableConfirms(true); RabbitMessageFuture future = this.asyncTemplate .sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties())); - ListenableFuture confirm = future.getConfirm(); - assertThat(confirm).isNotNull(); - assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue(); + CompletableFuture confirm = future.getConfirm(); + assertThat(confirm).isNotNull() + .succeedsWithin(Duration.ofSeconds(10)) + .isEqualTo(true); checkMessageResult(future, "SLEEP"); } @Test @DirtiesContext - public void testConvertWithConfirmDirect() throws Exception { + public void testConvertWithConfirmDirect() { this.asyncDirectTemplate.setEnableConfirms(true); RabbitConverterFuture future = this.asyncDirectTemplate.convertSendAndReceive("sleep"); - ListenableFuture confirm = future.getConfirm(); - assertThat(confirm).isNotNull(); - assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue(); + CompletableFuture confirm = future.getConfirm(); + assertThat(confirm).isNotNull() + .succeedsWithin(Duration.ofSeconds(10)) + .isEqualTo(true); checkConverterResult(future, "SLEEP"); } @@ -289,9 +309,10 @@ public void testMessageWithConfirmDirect() throws Exception { this.asyncDirectTemplate.setEnableConfirms(true); RabbitMessageFuture future = this.asyncDirectTemplate .sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties())); - ListenableFuture confirm = future.getConfirm(); - assertThat(confirm).isNotNull(); - assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue(); + CompletableFuture confirm = future.getConfirm(); + assertThat(confirm).isNotNull() + .succeedsWithin(Duration.ofSeconds(10)) + .isEqualTo(true); checkMessageResult(future, "SLEEP"); } @@ -300,18 +321,16 @@ public void testMessageWithConfirmDirect() throws Exception { @DirtiesContext public void testReceiveTimeout() throws Exception { this.asyncTemplate.setReceiveTimeout(500); - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("noReply"); + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("noReply"); TheCallback callback = new TheCallback(); - future.addCallback(callback); + future.whenComplete(callback); assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1); - try { - future.get(10, TimeUnit.SECONDS); - fail("Expected ExecutionException"); - } - catch (ExecutionException e) { - assertThat(e.getCause()).isInstanceOf(AmqpReplyTimeoutException.class); - } - assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(0); + assertThat(future) + .as("Expected ExecutionException") + .failsWithin(Duration.ofSeconds(10)) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(AmqpReplyTimeoutException.class); + assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).isEmpty(); assertThat(callback.latch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(callback.ex).isInstanceOf(AmqpReplyTimeoutException.class); } @@ -323,16 +342,15 @@ public void testReplyAfterReceiveTimeout() throws Exception { this.asyncTemplate.setReceiveTimeout(100); RabbitConverterFuture future = this.asyncTemplate.convertSendAndReceive("sleep"); TheCallback callback = new TheCallback(); - future.addCallback(callback); + future.whenComplete(callback); assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1); - try { - future.get(10, TimeUnit.SECONDS); - fail("Expected ExecutionException"); - } - catch (ExecutionException e) { - assertThat(e.getCause()).isInstanceOf(AmqpReplyTimeoutException.class); - } - assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(0); + + assertThat(future) + .as("Expected ExecutionException") + .failsWithin(Duration.ofSeconds(10)) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(AmqpReplyTimeoutException.class); + assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).isEmpty(); assertThat(callback.latch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(callback.ex).isInstanceOf(AmqpReplyTimeoutException.class); @@ -342,7 +360,7 @@ public void testReplyAfterReceiveTimeout() throws Exception { * map when it times out. However, there is a small race condition where * the reply arrives at the same time as the timeout. */ - future.set("foo"); + future.complete("foo"); assertThat(callback.result).isNull(); } @@ -353,21 +371,22 @@ public void testStopCancelled() throws Exception { this.asyncTemplate.setReceiveTimeout(5000); RabbitConverterFuture future = this.asyncTemplate.convertSendAndReceive("noReply"); TheCallback callback = new TheCallback(); - future.addCallback(callback); + future.whenComplete(callback); assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1); this.asyncTemplate.stop(); // Second stop() to be sure that it is idempotent this.asyncTemplate.stop(); - try { - future.get(10, TimeUnit.SECONDS); - fail("Expected CancellationException"); - } - catch (CancellationException e) { - assertThat(future.getNackCause()).isEqualTo("AsyncRabbitTemplate was stopped while waiting for reply"); - } - assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(0); + assertThat(future) + .as("Expected CancellationException") + .failsWithin(Duration.ofSeconds(10)) + .withThrowableOfType(CancellationException.class) + .satisfies(e -> { + assertThat(future.getNackCause()).isEqualTo("AsyncRabbitTemplate was stopped while waiting for reply"); + assertThat(future).isCancelled(); + }); + + assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).isEmpty(); assertThat(callback.latch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(future.isCancelled()).isTrue(); assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "taskScheduler")).isNull(); /* @@ -375,54 +394,116 @@ public void testStopCancelled() throws Exception { * should never happen because the container is stopped before canceling * and the future is removed from the pending map. */ - future.set("foo"); + future.complete("foo"); assertThat(callback.result).isNull(); } - private void checkConverterResult(ListenableFuture future, String expected) throws InterruptedException { - final CountDownLatch cdl = new CountDownLatch(1); - final AtomicReference resultRef = new AtomicReference<>(); - future.addCallback(new ListenableFutureCallback() { + @Test + @DirtiesContext + public void testConversionException() { + this.asyncTemplate.getRabbitTemplate().setMessageConverter(new SimpleMessageConverter() { @Override - public void onSuccess(String result) { - resultRef.set(result); - cdl.countDown(); + public Object fromMessage(Message message) throws MessageConversionException { + throw new MessageConversionException("Failed to convert message"); } + }); - @Override - public void onFailure(Throwable ex) { - cdl.countDown(); - } + RabbitConverterFuture replyFuture = this.asyncTemplate.convertSendAndReceive("conversionException"); - }); - assertThat(cdl.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(resultRef.get()).isEqualTo(expected); + assertThat(replyFuture).failsWithin(Duration.ofSeconds(10)) + .withThrowableThat() + .withCauseInstanceOf(MessageConversionException.class); } - private Message checkMessageResult(ListenableFuture future, String expected) throws InterruptedException { - final CountDownLatch cdl = new CountDownLatch(1); - final AtomicReference resultRef = new AtomicReference<>(); - future.addCallback(new ListenableFutureCallback() { + @Test + void ctorCoverage() { + AsyncRabbitTemplate template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk"); + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) + .extracting("exchange") + .isEqualTo("ex"); + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) + .extracting("routingKey") + .isEqualTo("rk"); + template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk", "rq"); + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) + .extracting("exchange") + .isEqualTo("ex"); + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) + .extracting("routingKey") + .isEqualTo("rk"); + assertThat(template) + .extracting("replyAddress") + .isEqualTo("rq"); + assertThat(template).extracting("container") + .extracting("queueNames") + .isEqualTo(new String[] {"rq"}); + template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk", "rq", "ra"); + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) + .extracting("exchange") + .isEqualTo("ex"); + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) + .extracting("routingKey") + .isEqualTo("rk"); + assertThat(template) + .extracting("replyAddress") + .isEqualTo("ra"); + assertThat(template).extracting("container") + .extracting("queueNames") + .isEqualTo(new String[] {"rq"}); + template = new AsyncRabbitTemplate(mock(RabbitTemplate.class), mock(AbstractMessageListenerContainer.class), + "rq"); + assertThat(template) + .extracting("replyAddress") + .isEqualTo("rq"); + } - @Override - public void onSuccess(Message result) { - resultRef.set(result); - cdl.countDown(); - } + @Test + public void limitedChannelsAreReleasedOnTimeout() { + CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost"); + connectionFactory.setChannelCacheSize(1); + connectionFactory.setChannelCheckoutTimeout(500L); + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + AsyncRabbitTemplate asyncRabbitTemplate = new AsyncRabbitTemplate(rabbitTemplate); + asyncRabbitTemplate.setReceiveTimeout(500L); + asyncRabbitTemplate.start(); + + RabbitConverterFuture replyFuture1 = asyncRabbitTemplate.convertSendAndReceive("noReply1"); + RabbitConverterFuture replyFuture2 = asyncRabbitTemplate.convertSendAndReceive("noReply2"); + + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> replyFuture1.get(10, TimeUnit.SECONDS)) + .withCauseInstanceOf(AmqpReplyTimeoutException.class); + + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> replyFuture2.get(10, TimeUnit.SECONDS)) + .withCauseInstanceOf(AmqpReplyTimeoutException.class); + + asyncRabbitTemplate.stop(); + connectionFactory.destroy(); + } - @Override - public void onFailure(Throwable ex) { - cdl.countDown(); - } + private void checkConverterResult(CompletableFuture future, String expected) { + assertThat(future) + .succeedsWithin(Duration.ofSeconds(10)) + .isEqualTo(expected); + } + private Message checkMessageResult(CompletableFuture future, String expected) throws InterruptedException { + final CountDownLatch cdl = new CountDownLatch(1); + final AtomicReference resultRef = new AtomicReference<>(); + future.whenComplete((result, ex) -> { + resultRef.set(result); + cdl.countDown(); }); assertThat(cdl.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(new String(resultRef.get().getBody())).isEqualTo(expected); + await().untilAsserted(() -> + assertThat(TestUtils.getPropertyValue(future, "timeoutTask", Future.class).isCancelled()).isTrue()); return resultRef.get(); } - public static class TheCallback implements ListenableFutureCallback { + public static class TheCallback implements BiConsumer { private final CountDownLatch latch = new CountDownLatch(1); @@ -431,13 +512,8 @@ public static class TheCallback implements ListenableFutureCallback { private volatile Throwable ex; @Override - public void onSuccess(String result) { + public void accept(String result, Throwable ex) { this.result = result; - latch.countDown(); - } - - @Override - public void onFailure(Throwable ex) { this.ex = ex; latch.countDown(); } @@ -504,24 +580,30 @@ public RabbitTemplate templateForDirect(ConnectionFactory connectionFactory) { @Primary public SimpleMessageListenerContainer replyContainer(ConnectionFactory connectionFactory) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setReceiveTimeout(10); container.setAfterReceivePostProcessors(new GUnzipPostProcessor()); container.setQueueNames(replies().getName()); return container; } @Bean - public AsyncRabbitTemplate asyncTemplate(RabbitTemplate template, SimpleMessageListenerContainer container) { + public AsyncRabbitTemplate asyncTemplate(@Qualifier("template") RabbitTemplate template, + SimpleMessageListenerContainer container) { + return new AsyncRabbitTemplate(template, container); } @Bean - public AsyncRabbitTemplate asyncDirectTemplate(RabbitTemplate templateForDirect) { + public AsyncRabbitTemplate asyncDirectTemplate( + @Qualifier("templateForDirect") RabbitTemplate templateForDirect) { + return new AsyncRabbitTemplate(templateForDirect); } @Bean public SimpleMessageListenerContainer remoteContainer(ConnectionFactory connectionFactory) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setReceiveTimeout(10); container.setQueueNames(requests().getName()); container.setAfterReceivePostProcessors(new GUnzipPostProcessor()); MessageListenerAdapter messageListener = diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java index 14bbb11813..88f40c7584 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,10 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.mock; - import java.util.Collection; import java.util.Map; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; @@ -42,12 +39,15 @@ import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; /** * * @author Stephane Nicoll * @author Gary Russell + * @author Ngoc Nhan */ public abstract class AbstractRabbitAnnotationDrivenTests { @@ -87,7 +87,9 @@ public void testSampleConfiguration(ApplicationContext context, int expectedDefa context.getBean("rabbitListenerContainerFactory", RabbitListenerContainerTestFactory.class); RabbitListenerContainerTestFactory simpleFactory = context.getBean("simpleFactory", RabbitListenerContainerTestFactory.class); + assertThat(defaultFactory.getBeanName()).isEqualTo("rabbitListenerContainerFactory"); assertThat(defaultFactory.getListenerContainers()).hasSize(expectedDefaultContainers); + assertThat(simpleFactory.getBeanName()).isEqualTo("simpleFactory"); assertThat(simpleFactory.getListenerContainers()).hasSize(1); Map queues = context .getBeansOfType(org.springframework.amqp.core.Queue.class); @@ -129,6 +131,7 @@ private void checkAdmin(Collection admins) { public void testFullConfiguration(ApplicationContext context) { RabbitListenerContainerTestFactory simpleFactory = context.getBean("simpleFactory", RabbitListenerContainerTestFactory.class); + assertThat(simpleFactory.getBeanName()).isEqualTo("simpleFactory"); assertThat(simpleFactory.getListenerContainers()).hasSize(1); MethodRabbitListenerEndpoint endpoint = (MethodRabbitListenerEndpoint) simpleFactory.getListenerContainers().get(0).getEndpoint(); @@ -142,6 +145,7 @@ public void testFullConfiguration(ApplicationContext context) { // Resolve the container and invoke a message on it SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setReceiveTimeout(10); endpoint.setupListenerContainer(container); MessagingMessageListenerAdapter listener = (MessagingMessageListenerAdapter) container.getMessageListener(); @@ -158,27 +162,30 @@ public void testFullConfiguration(ApplicationContext context) { } /** - * Test for {@link CustomBean} and an manually endpoint registered + * Test for {@link CustomBean} and a manually registered endpoint * with "myCustomEndpointId". The custom endpoint does not provide - * any factory so it's registered with the default one + * any factory, so it's registered with the default one */ public void testCustomConfiguration(ApplicationContext context) { RabbitListenerContainerTestFactory defaultFactory = context.getBean("rabbitListenerContainerFactory", RabbitListenerContainerTestFactory.class); RabbitListenerContainerTestFactory customFactory = context.getBean("customFactory", RabbitListenerContainerTestFactory.class); + assertThat(defaultFactory.getBeanName()).isEqualTo("rabbitListenerContainerFactory"); assertThat(defaultFactory.getListenerContainers()).hasSize(1); + assertThat(customFactory.getBeanName()).isEqualTo("customFactory"); assertThat(customFactory.getListenerContainers()).hasSize(1); RabbitListenerEndpoint endpoint = defaultFactory.getListenerContainers().get(0).getEndpoint(); assertThat(endpoint.getClass()).as("Wrong endpoint type").isEqualTo(SimpleRabbitListenerEndpoint.class); - assertThat(((SimpleRabbitListenerEndpoint) endpoint).getMessageListener()).as("Wrong listener set in custom endpoint").isEqualTo(context.getBean("simpleMessageListener")); + assertThat(((SimpleRabbitListenerEndpoint) endpoint).getMessageListener()) + .as("Wrong listener set in custom endpoint").isEqualTo(context.getBean("simpleMessageListener")); RabbitListenerEndpointRegistry customRegistry = context.getBean("customRegistry", RabbitListenerEndpointRegistry.class); - assertThat(customRegistry.getListenerContainerIds().size()).as("Wrong number of containers in the registry").isEqualTo(2); - assertThat(customRegistry.getListenerContainers().size()).as("Wrong number of containers in the registry").isEqualTo(2); - assertThat(customRegistry.getListenerContainer("listenerId")).as("Container with custom id on the annotation should be found").isNotNull(); - assertThat(customRegistry.getListenerContainer("myCustomEndpointId")).as("Container created with custom id should be found").isNotNull(); + assertThat(customRegistry.getListenerContainerIds()).hasSize(2); + assertThat(customRegistry.getListenerContainers()).hasSize(2); + assertThat(customRegistry.getListenerContainer("listenerId")).isNotNull(); + assertThat(customRegistry.getListenerContainer("myCustomEndpointId")).isNotNull(); } /** @@ -189,6 +196,7 @@ public void testCustomConfiguration(ApplicationContext context) { public void testExplicitContainerFactoryConfiguration(ApplicationContext context) { RabbitListenerContainerTestFactory defaultFactory = context.getBean("simpleFactory", RabbitListenerContainerTestFactory.class); + assertThat(defaultFactory.getBeanName()).isEqualTo("simpleFactory"); assertThat(defaultFactory.getListenerContainers()).hasSize(1); } @@ -205,7 +213,6 @@ public void testDefaultContainerFactoryConfiguration(ApplicationContext context) /** * Test for {@link ValidationBean} with a validator ({@link TestValidator}) specified * in a custom {@link org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory}. - * * The test should throw a {@link org.springframework.amqp.rabbit.support.ListenerExecutionFailedException} */ public void testRabbitHandlerMethodFactoryConfiguration(ApplicationContext context) throws Exception { @@ -216,6 +223,7 @@ public void testRabbitHandlerMethodFactoryConfiguration(ApplicationContext conte simpleFactory.getListenerContainers().get(0).getEndpoint(); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setReceiveTimeout(10); endpoint.setupListenerContainer(container); MessagingMessageListenerAdapter listener = (MessagingMessageListenerAdapter) container.getMessageListener(); @@ -274,6 +282,7 @@ public void defaultHandle(String msg) { @RabbitListener(containerFactory = "simpleFactory", queues = "myQueue") public void simpleHandle(String msg) { } + } @Component @@ -284,6 +293,7 @@ static class FullBean { public void fullHandle(String msg) { } + } @Component @@ -296,6 +306,7 @@ static class FullConfigurableBean { public void fullHandle(String msg) { } + } @Component @@ -304,6 +315,7 @@ static class CustomBean { @RabbitListener(id = "listenerId", containerFactory = "customFactory", queues = "myQueue") public void customHandle(String msg) { } + } static class DefaultBean { @@ -311,6 +323,7 @@ static class DefaultBean { @RabbitListener(queues = "myQueue") public void handleIt(String msg) { } + } @Component @@ -319,6 +332,7 @@ static class ValidationBean { @RabbitListener(containerFactory = "defaultFactory", queues = "myQueue") public void defaultHandle(@Validated String msg) { } + } @Component @@ -356,6 +370,7 @@ public void validate(Object target, Errors errors) { errors.reject("TEST: expected invalid value"); } } + } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AnnotationDrivenNamespaceTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AnnotationDrivenNamespaceTests.java index a180515af6..617bc5eda9 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AnnotationDrivenNamespaceTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AnnotationDrivenNamespaceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.annotation; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageListener; @@ -30,6 +27,8 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + /** * @author Stephane Nicoll * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java index d5ce86409a..cd4a3a6ded 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or 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,19 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.ImmediateRequeueAmqpException; @@ -36,7 +36,7 @@ import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.AsyncRabbitTemplate; -import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitConverterFuture; +import org.springframework.amqp.rabbit.RabbitConverterFuture; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -53,10 +53,8 @@ import org.springframework.stereotype.Component; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; -import reactor.core.publisher.Mono; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell @@ -105,7 +103,7 @@ public class AsyncListenerTests { private RabbitListenerEndpointRegistry registry; @Test - public void testAsyncListener() throws Exception { + public void testAsyncListener(@Autowired RabbitListenerEndpointRegistry registry) throws Exception { assertThat(this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo")).isEqualTo("FOO"); RabbitConverterFuture future = this.asyncTemplate.convertSendAndReceive(this.queue1.getName(), "foo"); assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("FOO"); @@ -119,6 +117,10 @@ public void testAsyncListener() throws Exception { assertThat(this.config.contentTypeId).isEqualTo("java.lang.String"); this.rabbitTemplate.convertAndSend(this.queue4.getName(), "foo"); assertThat(listener.latch4.await(10, TimeUnit.SECONDS)); + assertThat(TestUtils.getPropertyValue(registry.getListenerContainer("foo"), "asyncReplies", Boolean.class)) + .isTrue(); + assertThat(TestUtils.getPropertyValue(registry.getListenerContainer("bar"), "asyncReplies", Boolean.class)) + .isTrue(); } @Test @@ -284,13 +286,13 @@ public static class Listener { private final AtomicBoolean first7 = new AtomicBoolean(true); @RabbitListener(id = "foo", queues = "#{queue1.name}") - public ListenableFuture listen1(String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); + public CompletableFuture listen1(String foo) { + CompletableFuture future = new CompletableFuture<>(); if (fooFirst.getAndSet(false)) { - future.setException(new RuntimeException("Future.exception")); + future.completeExceptionally(new RuntimeException("Future.exception")); } else { - future.set(foo.toUpperCase()); + future.complete(foo.toUpperCase()); } return future; } @@ -311,17 +313,17 @@ public Mono> listen3(String foo) { } @RabbitListener(id = "qux", queues = "#{queue4.name}") - public ListenableFuture listen4(@SuppressWarnings("unused") String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); - future.set(null); + public CompletableFuture listen4(@SuppressWarnings("unused") String foo) { + CompletableFuture future = new CompletableFuture<>(); + future.complete(null); this.latch4.countDown(); return future; } @RabbitListener(id = "fiz", queues = "#{queue5.name}") - public ListenableFuture listen5(@SuppressWarnings("unused") String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); - future.setException(new AmqpRejectAndDontRequeueException("asyncToDLQ")); + public CompletableFuture listen5(@SuppressWarnings("unused") String foo) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new AmqpRejectAndDontRequeueException("asyncToDLQ")); return future; } @@ -331,9 +333,9 @@ public void listen5DLQ(@SuppressWarnings("unused") String foo) { } @RabbitListener(id = "fix", queues = "#{queue6.name}", containerFactory = "dontRequeueFactory") - public ListenableFuture listen6(@SuppressWarnings("unused") String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); - future.setException(new IllegalStateException("asyncDefaultToDLQ")); + public CompletableFuture listen6(@SuppressWarnings("unused") String foo) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalStateException("asyncDefaultToDLQ")); return future; } @@ -344,13 +346,13 @@ public void listen6DLQ(@SuppressWarnings("unused") String foo) { @RabbitListener(id = "overrideFactoryRequeue", queues = "#{queue7.name}", containerFactory = "dontRequeueFactory") - public ListenableFuture listen7(@SuppressWarnings("unused") String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); + public CompletableFuture listen7(@SuppressWarnings("unused") String foo) { + CompletableFuture future = new CompletableFuture<>(); if (this.first7.compareAndSet(true, false)) { - future.setException(new ImmediateRequeueAmqpException("asyncOverrideDefaultToDLQ")); + future.completeExceptionally(new ImmediateRequeueAmqpException("asyncOverrideDefaultToDLQ")); } else { - future.set("listen7"); + future.complete("listen7"); } return future; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java index 1c2a504f3a..18546a16f1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,12 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.AsyncRabbitTemplate; -import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitConverterFuture; +import org.springframework.amqp.rabbit.RabbitConverterFuture; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -42,6 +39,9 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + /** * @author Gary Russell * @since 2.0 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ConsumerBatchingTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ConsumerBatchingTests.java index 617fef6c1c..48373d1b28 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ConsumerBatchingTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ConsumerBatchingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2025 the original author or 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.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; @@ -27,6 +24,10 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.Channel; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpRejectAndDontRequeueException; @@ -48,10 +49,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.Channel; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java index 91a23421ba..76438d1267 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 the original author or 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.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -44,9 +42,12 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.MimeType; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.5 @@ -54,6 +55,7 @@ */ @RabbitAvailable @SpringJUnitConfig +@DirtiesContext public class ContentTypeDelegatingMessageConverterIntegrationTests { @Autowired diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java index 2c37e3b203..02c80cb434 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2025 the original author or 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.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy; @@ -44,6 +45,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.2 @@ -51,7 +54,7 @@ */ @SpringJUnitConfig @DirtiesContext -@RabbitAvailable(queues = { "batch.1", "batch.2", "batch.3", "batch.4" }) +@RabbitAvailable(queues = { "batch.1", "batch.2", "batch.3", "batch.4", "batch.5" }) public class EnableRabbitBatchIntegrationTests { @Autowired @@ -60,6 +63,11 @@ public class EnableRabbitBatchIntegrationTests { @Autowired private Listener listener; + @BeforeAll + static void setup() { + System.setProperty("spring.amqp.deserialization.trust.all", "true"); + } + @Test public void simpleList() throws InterruptedException { this.template.convertAndSend("batch.1", new Foo("foo")); @@ -114,6 +122,16 @@ public void nativeMessageList() throws InterruptedException { .isEqualTo(2); } + @Test + public void collectionWithStringInfer() throws InterruptedException { + this.template.convertAndSend("batch.5", new Foo("foo")); + this.template.convertAndSend("batch.5", new Foo("bar")); + assertThat(this.listener.fivesLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.listener.fives).hasSize(2); + assertThat(this.listener.fives.get(0).getBar()).isEqualTo("foo"); + assertThat(this.listener.fives.get(1).getBar()).isEqualTo("bar"); + } + @Configuration @EnableRabbit public static class Config { @@ -143,7 +161,7 @@ public DirectRabbitListenerContainerFactory directListenerContainerFactory() { public SimpleRabbitListenerContainerFactory consumerBatchContainerFactory() { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory()); - factory.setBatchListener(true); + factory.setBatchListener(false); factory.setConsumerBatchEnabled(true); factory.setBatchSize(2); return factory; @@ -186,6 +204,10 @@ public static class Listener { CountDownLatch fooConsumerBatchTooLatch = new CountDownLatch(1); + List fives = new ArrayList<>(); + + CountDownLatch fivesLatch = new CountDownLatch(1); + private List nativeMessages; private final CountDownLatch nativeMessagesLatch = new CountDownLatch(1); @@ -202,7 +224,7 @@ public void listen2(List> in) { this.fooMessagesLatch.countDown(); } - @RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory") + @RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory", batch = "true") public void listen3(List in) { this.foosConsumerBatchToo = in; this.fooConsumerBatchTooLatch.countDown(); @@ -214,6 +236,12 @@ public void listen4(List in) { this.nativeMessagesLatch.countDown(); } + @RabbitListener(queues = "batch.5") + public void listen5(Collection in) { + this.fives.addAll(in); + this.fivesLatch.countDown(); + } + } @SuppressWarnings("serial") diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java index 3c720291bc..4f5f92b780 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2025 the original author or 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.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.UnsupportedEncodingException; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -44,6 +42,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @author Kai Stapel diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitCglibProxyTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitCglibProxyTests.java index c0ab010464..fecca9e67e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitCglibProxyTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitCglibProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.io.Serializable; import org.junit.jupiter.api.Test; @@ -40,6 +37,9 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Artem Bilan * @since 1.5.5 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIdleContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIdleContainerTests.java index e01a2bfbe8..4f23fc61b1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIdleContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIdleContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -45,6 +43,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 1.6 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 277f2b5ed5..f407f6b03b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; - import java.io.IOException; import java.io.Serializable; import java.lang.annotation.ElementType; @@ -37,13 +31,17 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import javax.validation.Valid; - +import com.rabbitmq.client.Channel; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import jakarta.validation.Valid; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -60,6 +58,7 @@ import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerEndpoint; @@ -68,12 +67,14 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; import org.springframework.amqp.rabbit.connection.SimplePropertyValueConnectionNameStrategy; +import org.springframework.amqp.rabbit.core.NeedsManagementTests; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.BrokerRunningSupport; import org.springframework.amqp.rabbit.junit.LogLevels; import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler; import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; @@ -102,6 +103,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ApplicationContext; @@ -117,7 +119,7 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.task.TaskExecutor; import org.springframework.data.web.JsonPath; -import org.springframework.lang.NonNull; +import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.GenericMessageConverter; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Payload; @@ -140,12 +142,15 @@ import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; -import com.rabbitmq.client.Channel; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @@ -177,7 +182,7 @@ "test.custom.argument", "test.arg.validation", "manual.acks.1", "manual.acks.2", "erit.batch.1", "erit.batch.2", "erit.batch.3", "erit.mp.arg" }, purgeAfterEach = false) -public class EnableRabbitIntegrationTests { +public class EnableRabbitIntegrationTests extends NeedsManagementTests { @Autowired private RabbitTemplate rabbitTemplate; @@ -236,10 +241,14 @@ public class EnableRabbitIntegrationTests { @Autowired private MultiListenerValidatedJsonBean multiValidated; + @Autowired + private ReplyPostProcessor rpp; + @BeforeAll public static void setUp() { System.setProperty(RabbitListenerAnnotationBeanPostProcessor.RABBIT_EMPTY_STRING_ARGUMENTS_PROPERTY, "test-empty"); + System.setProperty("spring.amqp.deserialization.trust.all", "true"); RabbitAvailableCondition.getBrokerRunning().removeExchanges("auto.exch.tx", "auto.exch", "auto.exch.fanout", @@ -309,6 +318,8 @@ public void autoStart() { this.registry.start(); assertThat(listenerContainer.isRunning()).isTrue(); listenerContainer.stop(); + assertThat(listenerContainer.getMessageListener()).extracting("replyPostProcessor") + .isSameAs(this.rpp); } @Test @@ -383,6 +394,7 @@ public void multiListener() { rabbitTemplate.convertAndSend("multi.exch", "multi.rk", bar); assertThat(this.rabbitTemplate.receiveAndConvert("sendTo.replies")) .isEqualTo("CRASHCRASH Test reply from error handler"); + verify(this.multi, times(2)).bar(any()); bar.field = "bar"; Baz baz = new Baz(); baz.field = "baz"; @@ -398,7 +410,7 @@ public void multiListener() { this.rabbitTemplate.setAfterReceivePostProcessors(mpp); assertThat(rabbitTemplate.convertSendAndReceive("multi.exch", "multi.rk", qux)).isEqualTo("QUX: qux: multi.rk"); assertThat(beanMethodHeaders).hasSize(2); - assertThat(beanMethodHeaders.get(0)).isEqualTo("MultiListenerBean"); + assertThat(beanMethodHeaders.get(0)).contains("MultiListenerBean"); assertThat(beanMethodHeaders.get(1)).isEqualTo("qux"); this.rabbitTemplate.removeAfterReceivePostProcessor(mpp); assertThat(rabbitTemplate.convertSendAndReceive("multi.exch.tx", "multi.rk.tx", bar)).isEqualTo("BAR: barbar"); @@ -558,8 +570,9 @@ public void testRabbitHandlerNoDefaultValidationCount() throws InterruptedExcept public void testDifferentTypes() throws InterruptedException { Foo1 foo = new Foo1(); foo.setBar("bar"); + this.service.foos.clear(); this.jsonRabbitTemplate.convertAndSend("differentTypes", foo); - assertThat(this.service.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.service.dtLatch1.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.service.foos.get(0)).isInstanceOf(Foo2.class); assertThat(((Foo2) this.service.foos.get(0)).getBar()).isEqualTo("bar"); assertThat(TestUtils.getPropertyValue(this.registry.getListenerContainer("different"), "concurrentConsumers")).isEqualTo(2); @@ -569,8 +582,9 @@ public void testDifferentTypes() throws InterruptedException { public void testDifferentTypesWithConcurrency() throws InterruptedException { Foo1 foo = new Foo1(); foo.setBar("bar"); - this.jsonRabbitTemplate.convertAndSend("differentTypes", foo); - assertThat(this.service.latch.await(10, TimeUnit.SECONDS)).isTrue(); + this.service.foos.clear(); + this.jsonRabbitTemplate.convertAndSend("differentTypes2", foo); + assertThat(this.service.dtLatch2.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.service.foos.get(0)).isInstanceOf(Foo2.class); assertThat(((Foo2) this.service.foos.get(0)).getBar()).isEqualTo("bar"); MessageListenerContainer container = this.registry.getListenerContainer("differentWithConcurrency"); @@ -582,8 +596,9 @@ public void testDifferentTypesWithConcurrency() throws InterruptedException { public void testDifferentTypesWithVariableConcurrency() throws InterruptedException { Foo1 foo = new Foo1(); foo.setBar("bar"); - this.jsonRabbitTemplate.convertAndSend("differentTypes", foo); - assertThat(this.service.latch.await(10, TimeUnit.SECONDS)).isTrue(); + this.service.foos.clear(); + this.jsonRabbitTemplate.convertAndSend("differentTypes3", foo); + assertThat(this.service.dtLatch3.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.service.foos.get(0)).isInstanceOf(Foo2.class); assertThat(((Foo2) this.service.foos.get(0)).getBar()).isEqualTo("bar"); MessageListenerContainer container = this.registry.getListenerContainer("differentWithVariableConcurrency"); @@ -812,7 +827,7 @@ public void testMeta() throws Exception { } @Test - public void testHeadersExchange() throws Exception { + public void testHeadersExchange() { assertThat(rabbitTemplate.convertSendAndReceive("auto.headers", "", "foo", message -> { message.getMessageProperties().getHeaders().put("foo", "bar"); @@ -831,11 +846,10 @@ public void deadLetterOnDefaultExchange() { this.rabbitTemplate.convertAndSend("amqp656", "foo"); assertThat(this.rabbitTemplate.receiveAndConvert("amqp656dlq", 10000)).isEqualTo("foo"); try { - Client rabbitRestClient = new Client("http://localhost:15672/api/", "guest", "guest"); - QueueInfo amqp656 = rabbitRestClient.getQueue("/", "amqp656"); + Map amqp656 = await().until(() -> queueInfo("amqp656"), Objects::nonNull); if (amqp656 != null) { - assertThat(amqp656.getArguments().get("test-empty")).isEqualTo(""); - assertThat(amqp656.getArguments().get("test-null")).isEqualTo("undefined"); + assertThat(arguments(amqp656).get("test-empty")).isEqualTo(""); + assertThat(arguments(amqp656).get("test-null")).isEqualTo("undefined"); } } catch (Exception e) { @@ -947,7 +961,7 @@ public void messagingMessageReturned() throws InterruptedException { catch (@SuppressWarnings("unused") Exception e) { return null; } - }, tim -> tim != null); + }, Objects::nonNull); assertThat(timer.count()).isEqualTo(1L); } @@ -1017,6 +1031,13 @@ void messagePropertiesParam() { })).isEqualTo("foo, myProp=bar"); } + @Test + void listenerWithBrokerNamedQueue() { + AbstractMessageListenerContainer container = + (AbstractMessageListenerContainer) this.registry.getListenerContainer("brokerNamed"); + assertThat(container.getQueueNames()[0]).startsWith("amq.gen"); + } + interface TxService { @Transactional @@ -1077,7 +1098,11 @@ public static class MyService { final List foos = new ArrayList<>(); - final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch dtLatch1 = new CountDownLatch(1); + + final CountDownLatch dtLatch2 = new CountDownLatch(1); + + final CountDownLatch dtLatch3 = new CountDownLatch(1); final CountDownLatch validationLatch = new CountDownLatch(1); @@ -1179,7 +1204,7 @@ public String multiQueuesConfig(String foo) { } @RabbitListener(queues = "test.header", group = "testGroup", replyPostProcessor = "#{'echoPrefixHeader'}") - public String capitalizeWithHeader(@Payload String content, @Header String prefix) { + public String capitalizeWithHeader(@Payload String content, @Header("prefix") String prefix) { return prefix + content.toUpperCase(); } @@ -1189,7 +1214,7 @@ public String capitalizeWithMessage(org.springframework.messaging.Message reply(String payload, @Header String foo, + public org.springframework.messaging.Message reply(String payload, @Header("foo") String foo, @Header(AmqpHeaders.CONSUMER_TAG) String tag) { return MessageBuilder.withPayload(payload) .setHeader("foo", foo).setHeader("bar", tag).build(); @@ -1228,21 +1253,21 @@ public void handleIt(Date body) { containerFactory = "jsonListenerContainerFactoryNoClassMapper") public void handleDifferent(@Validated Foo2 foo) { foos.add(foo); - latch.countDown(); + dtLatch1.countDown(); } @RabbitListener(id = "differentWithConcurrency", queues = "differentTypes2", - containerFactory = "jsonListenerContainerFactory", concurrency = "#{3}") - public void handleDifferentWithConcurrency(Foo2 foo) { + containerFactory = "jsonListenerContainerFactoryNoClassMapper", concurrency = "#{3}") + public void handleDifferentWithConcurrency(Foo2 foo, MessageHeaders headers) { foos.add(foo); - latch.countDown(); + dtLatch2.countDown(); } @RabbitListener(id = "differentWithVariableConcurrency", queues = "differentTypes3", containerFactory = "jsonListenerContainerFactory", concurrency = "3-4") public void handleDifferentWithVariableConcurrency(Foo2 foo) { foos.add(foo); - latch.countDown(); + dtLatch3.countDown(); } @RabbitListener(id = "notStarted", containerFactory = "rabbitAutoStartFalseListenerContainerFactory", @@ -1419,6 +1444,10 @@ public String mpArgument(String payload, MessageProperties props) { return payload + ", myProp=" + props.getHeader("myProp"); } + @RabbitListener(id = "brokerNamed", queues = "#{@brokerNamed}") + void brokerNamed(String in) { + } + } public static class JsonObject { @@ -1649,7 +1678,9 @@ public SimpleRabbitListenerContainerFactory txListenerContainerFactory() { @Bean public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener( + @Qualifier("rabbitListenerContainerFactory") SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { + SimpleRabbitListenerEndpoint listener = new SimpleRabbitListenerEndpoint(); listener.setQueueNames("test.manual.container"); listener.setMessageListener((ChannelAwareMessageListener) (message, channel) -> { @@ -1661,6 +1692,7 @@ public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener( @Bean public SimpleMessageListenerContainer factoryCreatedContainerNoListener( + @Qualifier("rabbitListenerContainerFactory") SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { SimpleMessageListenerContainer container = rabbitListenerContainerFactory.createListenerContainer(); container.setMessageListener(message -> { @@ -1672,14 +1704,22 @@ public SimpleMessageListenerContainer factoryCreatedContainerNoListener( } @Bean - public SimpleRabbitListenerContainerFactory rabbitAutoStartFalseListenerContainerFactory() { + public SimpleRabbitListenerContainerFactory rabbitAutoStartFalseListenerContainerFactory( + @Qualifier("rpp") ReplyPostProcessor rpp) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(rabbitConnectionFactory()); factory.setReceiveTimeout(10L); factory.setAutoStartup(false); + factory.setReplyPostProcessorProvider(id -> rpp); return factory; } + @Bean + ReplyPostProcessor rpp() { + return (in, out) -> out; + } + @Bean public SimpleRabbitListenerContainerFactory jsonListenerContainerFactory() { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); @@ -1750,6 +1790,7 @@ public SimpleRabbitListenerContainerFactory consumerBatchContainerFactory() { factory.setBatchListener(true); factory.setBatchSize(2); factory.setConsumerBatchEnabled(true); + factory.setReceiveTimeout(10L); return factory; } @@ -1763,12 +1804,12 @@ public boolean supports(Class clazz) { @Override public void validate(Object target, Errors errors) { - if (target instanceof ValidatedClass) { - if (((ValidatedClass) target).getBar() > 10) { + if (target instanceof ValidatedClass validatedClass) { + if (validatedClass.getBar() > 10) { errors.reject("bar too large"); } else { - ((ValidatedClass) target).setValidated(true); + validatedClass.setValidated(true); } } } @@ -1825,7 +1866,7 @@ public CountDownLatch errorHandlerLatch2() { @Bean public AtomicReference errorHandlerError() { - return new AtomicReference(); + return new AtomicReference<>(); } @Bean @@ -1864,12 +1905,12 @@ public MyService myService() { @Bean public RabbitListenerErrorHandler alwaysBARHandler() { - return (msg, springMsg, ex) -> "BAR"; + return (msg, channel, springMsg, ex) -> "BAR"; } @Bean public RabbitListenerErrorHandler upcaseAndRepeatErrorHandler() { - return (msg, springMsg, ex) -> { + return (msg, channel, springMsg, ex) -> { String payload = ((Bar) springMsg.getPayload()).field.toUpperCase(); return payload + payload + " " + ex.getCause().getMessage(); }; @@ -1877,15 +1918,15 @@ public RabbitListenerErrorHandler upcaseAndRepeatErrorHandler() { @Bean public RabbitListenerErrorHandler throwANewException() { - return (msg, springMsg, ex) -> { - this.errorHandlerChannel = springMsg.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class); + return (msg, channel, springMsg, ex) -> { + this.errorHandlerChannel = channel; throw new RuntimeException("from error handler", ex.getCause()); }; } @Bean public RabbitListenerErrorHandler throwWrappedValidationException() { - return (msg, springMsg, ex) -> { + return (msg, channel, springMsg, ex) -> { throw new RuntimeException("argument validation failed", ex); }; } @@ -1907,7 +1948,9 @@ public TxService txService() { @Bean public TaskExecutor exec1() { - return new ThreadPoolTaskExecutor(); + ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.setAcceptTasksAfterContextClose(true); + return threadPoolTaskExecutor; } // Rabbit infrastructure setup @@ -1947,7 +1990,7 @@ public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) { @Bean public MultiListenerBean multiListener() { - return new MultiListenerBean(); + return spy(new MultiListenerBean()); } @Bean @@ -2010,6 +2053,11 @@ public ReplyPostProcessor echoPrefixHeader() { }; } + @Bean + org.springframework.amqp.core.Queue brokerNamed() { + return QueueBuilder.nonDurable("").autoDelete().exclusive().build(); + } + } @RabbitListener(bindings = @QueueBinding @@ -2024,7 +2072,7 @@ static class MultiListenerBean { @RabbitHandler @SendTo("${foo.bar:#{sendToRepliesBean}}") - public String bar(@NonNull Bar bar) { + public String bar(Bar bar) { if (bar.field.equals("crash")) { throw new RuntimeException("Test reply from error handler"); } @@ -2039,14 +2087,14 @@ public String baz(Baz baz, Message message) { } @RabbitHandler - public String qux(@Header("amqp_receivedRoutingKey") String rk, @NonNull @Payload Qux qux) { + public String qux(@Header("amqp_receivedRoutingKey") String rk, @Payload Qux qux) { return "QUX: " + qux.field + ": " + rk; } @RabbitHandler(isDefault = true) public String defaultHandler(@Payload Object payload) { - if (payload instanceof Foo) { - return "FOO: " + ((Foo) payload).field + " handled by default handler"; + if (payload instanceof Foo foo) { + return "FOO: " + foo.field + " handled by default handler"; } return payload.toString() + " handled by default handler"; } @@ -2425,14 +2473,14 @@ public String messagingMessage(org.springframework.messaging.Message message) @RabbitListener(queues = "test.converted.foomessage") public String messagingMessage(org.springframework.messaging.Message message, - @Header(value = "", required = false) String h, + @Header(value = "notPresent", required = false) String h, @Header(name = AmqpHeaders.RECEIVED_USER_ID) String userId) { return message.getClass().getSimpleName() + message.getPayload().getClass().getSimpleName() + userId; } @RabbitListener(queues = "test.notconverted.messagingmessagenotgeneric") public String messagingMessage(@SuppressWarnings("rawtypes") org.springframework.messaging.Message message, - @Header(value = "", required = false) Integer h) { + @Header(value = "notPresent", required = false) Integer h) { return message.getClass().getSimpleName() + message.getPayload().getClass().getSimpleName(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java index da80c5e2c6..5cd07d9352 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2025 the original author or 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.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.List; @@ -44,6 +42,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.2 @@ -113,6 +113,7 @@ public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(Cachi public RabbitTemplate template(CachingConnectionFactory cf, Jackson2JsonMessageConverter converter) { RabbitTemplate template = new RabbitTemplate(cf); template.setMessageConverter(converter); + template.setReplyTimeout(30_000); return template; } @@ -143,7 +144,7 @@ public ReplyPostProcessor rpp() { @Bean public RabbitListenerErrorHandler rleh() { - return (amqpMessage, message, exception) -> null; + return (amqpMessage, channel, message, exception) -> null; } @RabbitListener(queues = "EnableRabbitReturnTypesTests.1", admin = "#{@admin}", diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitTests.java index b7816201b8..2f10a27494 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,7 @@ package org.springframework.amqp.rabbit.annotation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; - +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageListener; @@ -54,7 +46,13 @@ import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; import org.springframework.stereotype.Component; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; /** * @author Stephane Nicoll diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/LazyContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/LazyContainerTests.java index d6be1c75bb..0e7a97159d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/LazyContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/LazyContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author or 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.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -36,6 +34,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.1.5 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MessageHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MessageHandlerTests.java index bc918fbaa3..bd629702c8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MessageHandlerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MessageHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,13 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageProperties; @@ -39,7 +37,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.ReflectionUtils; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java index 3aa30d5845..d0900ecd1b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2025 the original author 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.function.BiFunction; import java.util.stream.Collectors; +import com.rabbitmq.client.Channel; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -53,8 +54,6 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.stereotype.Component; -import com.rabbitmq.client.Channel; - /** * @author Wander Costa */ @@ -68,7 +67,7 @@ void multipleSimpleMessageListeners() { Map factories = context .getBeansOfType(RabbitListenerContainerTestFactory.class, false, false); - Assertions.assertThat(factories).hasSize(3); + Assertions.assertThat(factories).hasSize(4); factories.values().forEach(factory -> { Assertions.assertThat(factory.getListenerContainers().size()) @@ -83,6 +82,7 @@ void multipleSimpleMessageListeners() { Assertions.assertThat(methodEndpoint.getMethod()).isNotNull(); SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); + listenerContainer.setReceiveTimeout(10); methodEndpoint.setupListenerContainer(listenerContainer); Assertions.assertThat(listenerContainer.getMessageListener()).isNotNull(); }); @@ -98,34 +98,38 @@ void testDeclarablesMatchProperRabbitAdmin() { Map factories = context .getBeansOfType(RabbitListenerContainerTestFactory.class, false, false); - Assertions.assertThat(factories).hasSize(3); + Assertions.assertThat(factories).hasSize(4); BiFunction declares = (admin, dec) -> dec.getDeclaringAdmins().size() == 1 && dec.getDeclaringAdmins().contains(admin.getBeanName()); Map exchanges = context.getBeansOfType(AbstractExchange.class, false, false) .values().stream().collect(Collectors.toMap(AbstractExchange::getName, v -> v)); - Assertions.assertThat(exchanges).hasSize(3); + Assertions.assertThat(exchanges).hasSize(4); Assertions.assertThat(declares.apply(MultiConfig.DEFAULT_RABBIT_ADMIN, exchanges.get("testExchange"))).isTrue(); Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_B, exchanges.get("testExchangeB"))) .isTrue(); Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_C, exchanges.get("testExchangeC"))) .isTrue(); + Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_D, exchanges.get("testExchangeD"))) + .isTrue(); Map queues = context .getBeansOfType(org.springframework.amqp.core.Queue.class, false, false) .values().stream().collect(Collectors.toMap(org.springframework.amqp.core.Queue::getName, v -> v)); - Assertions.assertThat(queues).hasSize(3); + Assertions.assertThat(queues).hasSize(4); Assertions.assertThat(declares.apply(MultiConfig.DEFAULT_RABBIT_ADMIN, queues.get("testQueue"))).isTrue(); Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_B, queues.get("testQueueB"))).isTrue(); Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_C, queues.get("testQueueC"))).isTrue(); + Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_D, queues.get("testQueueD"))).isTrue(); Map bindings = context.getBeansOfType(Binding.class, false, false) .values().stream().collect(Collectors.toMap(Binding::getRoutingKey, v -> v)); - Assertions.assertThat(bindings).hasSize(3); + Assertions.assertThat(bindings).hasSize(4); Assertions.assertThat(declares.apply(MultiConfig.DEFAULT_RABBIT_ADMIN, bindings.get("testKey"))).isTrue(); Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_B, bindings.get("testKeyB"))).isTrue(); Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_C, bindings.get("testKeyC"))).isTrue(); + Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_D, bindings.get("testKeyD"))).isTrue(); context.close(); // Close and stop the listeners } @@ -179,9 +183,19 @@ void testCreationOfConnections() { Mockito.verify(MultiConfig.CONNECTION_FACTORY_BROKER_C).createConnection(); Mockito.verify(MultiConfig.CONNECTION_BROKER_C).createChannel(false); + Mockito.verify(MultiConfig.CONNECTION_FACTORY_BROKER_D, Mockito.never()).createConnection(); + Mockito.verify(MultiConfig.CONNECTION_BROKER_D, Mockito.never()).createChannel(false); + SimpleResourceHolder.bind(MultiConfig.ROUTING_CONNECTION_FACTORY, "brokerD"); + rabbitTemplate.convertAndSend("messageToBrokerD"); + SimpleResourceHolder.unbind(MultiConfig.ROUTING_CONNECTION_FACTORY); + Mockito.verify(MultiConfig.CONNECTION_FACTORY_BROKER_D).createConnection(); + Mockito.verify(MultiConfig.CONNECTION_BROKER_D).createChannel(false); + context.close(); // Close and stop the listeners } + + @Test @DisplayName("Test assignment of RabbitAdmin in the endpoint registry") void testAssignmentOfRabbitAdminInTheEndpointRegistry() { @@ -191,7 +205,7 @@ void testAssignmentOfRabbitAdminInTheEndpointRegistry() { final RabbitListenerEndpointRegistry registry = context.getBean(RabbitListenerEndpointRegistry.class); final Collection listenerContainers = registry.getListenerContainers(); - Assertions.assertThat(listenerContainers).hasSize(3); + Assertions.assertThat(listenerContainers).hasSize(4); listenerContainers.forEach(container -> { Assertions.assertThat(container).isInstanceOf(MessageListenerTestContainer.class); final MessageListenerTestContainer refContainer = (MessageListenerTestContainer) container; @@ -227,6 +241,13 @@ public void handleItB(String body) { key = "testKeyC")) public void handleItC(String body) { } + + @RabbitListener(containerFactory = "${broker-name:brokerD}", bindings = @QueueBinding( + exchange = @Exchange("testExchangeD"), + value = @Queue("testQueueD"), + key = "testKeyD")) + public void handleItD(String body) { + } } @Component @@ -243,6 +264,10 @@ public void handleItB(String body) { @RabbitListener(queues = "testQueueC", containerFactory = "brokerC") public void handleItC(String body) { } + + @RabbitListener(queues = "testQueueD", containerFactory = "${broker-name:brokerD}") + public void handleItD(String body) { + } } @Configuration @@ -253,34 +278,41 @@ static class MultiConfig { static final ConnectionFactory DEFAULT_CONNECTION_FACTORY = Mockito.mock(ConnectionFactory.class); static final ConnectionFactory CONNECTION_FACTORY_BROKER_B = Mockito.mock(ConnectionFactory.class); static final ConnectionFactory CONNECTION_FACTORY_BROKER_C = Mockito.mock(ConnectionFactory.class); + static final ConnectionFactory CONNECTION_FACTORY_BROKER_D = Mockito.mock(ConnectionFactory.class); static final Connection DEFAULT_CONNECTION = Mockito.mock(Connection.class); static final Connection CONNECTION_BROKER_B = Mockito.mock(Connection.class); static final Connection CONNECTION_BROKER_C = Mockito.mock(Connection.class); + static final Connection CONNECTION_BROKER_D = Mockito.mock(Connection.class); static final Channel DEFAULT_CHANNEL = Mockito.mock(Channel.class); static final Channel CHANNEL_BROKER_B = Mockito.mock(Channel.class); static final Channel CHANNEL_BROKER_C = Mockito.mock(Channel.class); + static final Channel CHANNEL_BROKER_D = Mockito.mock(Channel.class); static { final Map targetConnectionFactories = new HashMap<>(); targetConnectionFactories.put("brokerB", CONNECTION_FACTORY_BROKER_B); targetConnectionFactories.put("brokerC", CONNECTION_FACTORY_BROKER_C); + targetConnectionFactories.put("brokerD", CONNECTION_FACTORY_BROKER_D); ROUTING_CONNECTION_FACTORY.setDefaultTargetConnectionFactory(DEFAULT_CONNECTION_FACTORY); ROUTING_CONNECTION_FACTORY.setTargetConnectionFactories(targetConnectionFactories); Mockito.when(DEFAULT_CONNECTION_FACTORY.createConnection()).thenReturn(DEFAULT_CONNECTION); Mockito.when(CONNECTION_FACTORY_BROKER_B.createConnection()).thenReturn(CONNECTION_BROKER_B); Mockito.when(CONNECTION_FACTORY_BROKER_C.createConnection()).thenReturn(CONNECTION_BROKER_C); + Mockito.when(CONNECTION_FACTORY_BROKER_D.createConnection()).thenReturn(CONNECTION_BROKER_D); Mockito.when(DEFAULT_CONNECTION.createChannel(false)).thenReturn(DEFAULT_CHANNEL); Mockito.when(CONNECTION_BROKER_B.createChannel(false)).thenReturn(CHANNEL_BROKER_B); Mockito.when(CONNECTION_BROKER_C.createChannel(false)).thenReturn(CHANNEL_BROKER_C); + Mockito.when(CONNECTION_BROKER_D.createChannel(false)).thenReturn(CHANNEL_BROKER_D); } static final RabbitAdmin DEFAULT_RABBIT_ADMIN = new RabbitAdmin(DEFAULT_CONNECTION_FACTORY); static final RabbitAdmin RABBIT_ADMIN_BROKER_B = new RabbitAdmin(CONNECTION_FACTORY_BROKER_B); static final RabbitAdmin RABBIT_ADMIN_BROKER_C = new RabbitAdmin(CONNECTION_FACTORY_BROKER_C); + static final RabbitAdmin RABBIT_ADMIN_BROKER_D = new RabbitAdmin(CONNECTION_FACTORY_BROKER_D); @Bean public RabbitListenerAnnotationBeanPostProcessor postProcessor() { @@ -306,6 +338,11 @@ public RabbitAdmin rabbitAdminBrokerC() { return RABBIT_ADMIN_BROKER_C; } + @Bean("brokerD-admin") + public RabbitAdmin rabbitAdminBrokerD() { + return RABBIT_ADMIN_BROKER_D; + } + @Bean("defaultContainerFactory") public RabbitListenerContainerTestFactory defaultContainerFactory() { return new RabbitListenerContainerTestFactory(); @@ -321,6 +358,11 @@ public RabbitListenerContainerTestFactory containerFactoryBrokerC() { return new RabbitListenerContainerTestFactory(); } + @Bean("brokerD") + public RabbitListenerContainerTestFactory containerFactoryBrokerD() { + return new RabbitListenerContainerTestFactory(); + } + @Bean public RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry() { return new RabbitListenerEndpointRegistry(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTest.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTests.java similarity index 96% rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTest.java rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTests.java index 39388710dc..dc495ead2e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTest.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -28,7 +26,9 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.env.Environment; -class MultiRabbitBootstrapConfigurationTest { +import static org.assertj.core.api.Assertions.assertThat; + +class MultiRabbitBootstrapConfigurationTests { @Test @DisplayName("test if MultiRabbitBPP is registered when enabled") @@ -67,4 +67,5 @@ void testMultiRabbitBPPIsNotRegistered() throws Exception { Mockito.verify(registry, Mockito.never()).registerBeanDefinition(Mockito.anyString(), Mockito.any(RootBeanDefinition.class)); } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java new file mode 100644 index 0000000000..b699875409 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.annotation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.AmqpException; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @since 2.8 + * + */ +@SpringJUnitConfig +@RabbitAvailable(queues = { "op.1", "op.2" }) +@DirtiesContext +public class OptionalPayloadTests { + + @Test + void optionals(@Autowired RabbitTemplate template, @Autowired Listener listener) + throws JsonProcessingException, AmqpException, InterruptedException { + + ObjectMapper objectMapper = new ObjectMapper(); + template.send("op.1", MessageBuilder.withBody(objectMapper.writeValueAsBytes("foo")) + .andProperties(MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .build()) + .build()); + template.send("op.1", MessageBuilder.withBody(objectMapper.writeValueAsBytes(null)) + .andProperties(MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .build()) + .build()); + template.send("op.2", MessageBuilder.withBody(objectMapper.writeValueAsBytes("bar")) + .andProperties(MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .build()) + .build()); + template.send("op.2", MessageBuilder.withBody(objectMapper.writeValueAsBytes(null)) + .andProperties(MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .build()) + .build()); + assertThat(listener.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(listener.deOptionaled).containsExactlyInAnyOrder("foo", null, "bar", "baz"); + } + + @Configuration + @EnableRabbit + public static class Config { + + @Bean + RabbitTemplate template() { + return new RabbitTemplate(rabbitConnectionFactory()); + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(rabbitConnectionFactory()); + factory.setMessageConverter(converter()); + return factory; + } + + @Bean + ConnectionFactory rabbitConnectionFactory() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + Jackson2JsonMessageConverter converter() { + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + converter.setNullAsOptionalEmpty(true); + return converter; + } + + @Bean + Listener listener() { + return new Listener(); + } + + } + + static class Listener { + + final CountDownLatch latch = new CountDownLatch(4); + + List deOptionaled = new ArrayList<>(); + + @RabbitListener(queues = "op.1") + void listen(@Payload(required = false) String payload) { + this.deOptionaled.add(payload); + this.latch.countDown(); + } + + @RabbitListener(queues = "op.2") + void listen(Optional optional) { + this.deOptionaled.add(optional.orElse("baz")); + this.latch.countDown(); + } + + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java index b7a06e28bc..72cea92b57 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; @@ -59,6 +57,8 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Stephane Nicoll * @author Juergen Hoeller @@ -86,6 +86,7 @@ public void simpleMessageListener() { assertThat(methodEndpoint.getMethod()).isNotNull(); SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); + listenerContainer.setReceiveTimeout(10); methodEndpoint.setupListenerContainer(listenerContainer); assertThat(listenerContainer.getMessageListener()).isNotNull(); @@ -114,6 +115,7 @@ public void simpleMessageListenerWithMixedAnnotations() { assertThat(iterator.next()).isEqualTo("secondQueue"); SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); + listenerContainer.setReceiveTimeout(10); methodEndpoint.setupListenerContainer(listenerContainer); assertThat(listenerContainer.getMessageListener()).isNotNull(); @@ -187,9 +189,9 @@ public void multipleQueuesTestBean() { RabbitListenerContainerTestFactory factory = context.getBean(RabbitListenerContainerTestFactory.class); assertThat(factory.getListenerContainers().size()).as("one container should have been registered").isEqualTo(1); RabbitListenerEndpoint endpoint = factory.getListenerContainers().get(0).getEndpoint(); - final Iterator iterator = ((AbstractRabbitListenerEndpoint) endpoint).getQueueNames().iterator(); - assertThat(iterator.next()).isEqualTo("testQueue"); - assertThat(iterator.next()).isEqualTo("secondQueue"); + final Iterator iterator = ((AbstractRabbitListenerEndpoint) endpoint).getQueues().iterator(); + assertThat(iterator.next().getName()).isEqualTo("testQueue"); + assertThat(iterator.next().getName()).isEqualTo("secondQueue"); context.close(); } @@ -218,9 +220,9 @@ public void propertyResolvingToExpressionTestBean() { RabbitListenerContainerTestFactory factory = context.getBean(RabbitListenerContainerTestFactory.class); assertThat(factory.getListenerContainers().size()).as("one container should have been registered").isEqualTo(1); RabbitListenerEndpoint endpoint = factory.getListenerContainers().get(0).getEndpoint(); - final Iterator iterator = ((AbstractRabbitListenerEndpoint) endpoint).getQueueNames().iterator(); - assertThat(iterator.next()).isEqualTo("testQueue"); - assertThat(iterator.next()).isEqualTo("secondQueue"); + final Iterator iterator = ((AbstractRabbitListenerEndpoint) endpoint).getQueues().iterator(); + assertThat(iterator.next().getName()).isEqualTo("testQueue"); + assertThat(iterator.next().getName()).isEqualTo("secondQueue"); context.close(); } @@ -388,7 +390,7 @@ public void handleIt(String body) { @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Repeatable(FooListeners.class) - static @interface FooListener { + @interface FooListener { @AliasFor(annotation = RabbitListener.class, attribute = "queues") String[] value() default {}; @@ -397,7 +399,7 @@ public void handleIt(String body) { @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) - static @interface FooListeners { + @interface FooListeners { FooListener[] value(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/AdminParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/AdminParserTests.java index 48d07c4bd3..226ddfad93 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/AdminParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/AdminParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; @@ -33,6 +30,9 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.util.StringUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * * @author tomas.lukosius@opencredo.com diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java new file mode 100644 index 0000000000..356a1d9909 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.config; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Gary Russell + * @since 2.4.8 + * + */ +public class CompositeContainerCustomizerTests { + + @SuppressWarnings("unchecked") + @Test + void allCalled() { + ContainerCustomizer mock1 = mock(ContainerCustomizer.class); + ContainerCustomizer mock2 = mock(ContainerCustomizer.class); + CompositeContainerCustomizer cust = new CompositeContainerCustomizer<>( + List.of(mock1, mock2)); + MessageListenerContainer mlc = mock(MessageListenerContainer.class); + cust.configure(mlc); + verify(mock1).configure(mlc); + verify(mock2).configure(mlc); + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests.java index 20256ff2f8..e0ef32f967 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.List; import java.util.concurrent.ExecutorService; +import com.rabbitmq.client.Address; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,8 +35,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import com.rabbitmq.client.Address; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; /** * diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserIntegrationTests.java index 652134a69e..113b0d251e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -33,6 +31,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserTests.java index db6dee3814..02da3d145b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -32,6 +30,8 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Mark Fisher diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java new file mode 100644 index 0000000000..4fb1b54f2e --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.config; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.config.ListenerContainerFactoryBean.Type; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.utils.test.TestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * @author Gary Russell + * @since 2.4.6 + * + */ +public class ListenerContainerFactoryBeanTests { + + @SuppressWarnings("unchecked") + @Test + void micrometer() throws Exception { + ListenerContainerFactoryBean lcfb = new ListenerContainerFactoryBean(); + lcfb.setConnectionFactory(mock(ConnectionFactory.class)); + lcfb.setMicrometerEnabled(false); + lcfb.setMicrometerTags(Map.of("foo", "bar")); + lcfb.afterPropertiesSet(); + AbstractMessageListenerContainer container = lcfb.getObject(); + assertThat(TestUtils.getPropertyValue(container, "micrometerEnabled", Boolean.class)).isFalse(); + assertThat(TestUtils.getPropertyValue(container, "micrometerTags", Map.class)).hasSize(1); + } + + @Test + void smlcCustomizer() throws Exception { + ListenerContainerFactoryBean lcfb = new ListenerContainerFactoryBean(); + lcfb.setConnectionFactory(mock(ConnectionFactory.class)); + lcfb.setSMLCCustomizer(container -> { + container.setConsumerStartTimeout(42L); + }); + lcfb.afterPropertiesSet(); + AbstractMessageListenerContainer container = lcfb.getObject(); + assertThat(TestUtils.getPropertyValue(container, "consumerStartTimeout", Long.class)).isEqualTo(42L); + } + + @Test + void dmlcCustomizer() throws Exception { + ListenerContainerFactoryBean lcfb = new ListenerContainerFactoryBean(); + lcfb.setConnectionFactory(mock(ConnectionFactory.class)); + lcfb.setType(Type.direct); + lcfb.setDMLCCustomizer(container -> { + container.setConsumersPerQueue(2); + }); + lcfb.afterPropertiesSet(); + AbstractMessageListenerContainer container = lcfb.getObject(); + assertThat(TestUtils.getPropertyValue(container, "consumersPerQueue", Integer.class)).isEqualTo(2); + } + + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerParserTests.java index 28b5a9cf10..727b773e8f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2021 the original author or authors. + * Copyright 2010-2025 the original author or 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.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; @@ -46,6 +43,9 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * @author Mark Fisher * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java index 5baf91f756..0d55a932f8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 the original author or authors. + * Copyright 2010-2025 the original author or 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.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; @@ -39,6 +37,8 @@ import org.springframework.context.support.GenericXmlApplicationContext; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Gary Russell @@ -82,4 +82,21 @@ public void testParseWithQueueNames() throws Exception { assertThat(Arrays.asList(container.getQueueNames()).toString()).isEqualTo("[foo, " + queue.getName() + "]"); } + @Test + public void commasInPropertyNames() { + SimpleMessageListenerContainer container = this.context.getBean("commaProps1", + SimpleMessageListenerContainer.class); + assertThat(container.getQueueNames()).containsExactly("foo", "bar"); + } + + @Test + public void commasInPropertyQueues() { + SimpleMessageListenerContainer container = this.context.getBean("commaProps2", + SimpleMessageListenerContainer.class); + String[] queueNames = container.getQueueNames(); + assertThat(queueNames).hasSize(2); + assertThat(queueNames[0]).isEqualTo("foo"); + assertThat(queueNames[1]).startsWith("spring.gen"); + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MessageListenerTestContainer.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MessageListenerTestContainer.java index 16c6e2960d..e014ddfaa7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MessageListenerTestContainer.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MessageListenerTestContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,13 @@ package org.springframework.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** * @author Stephane Nicoll @@ -57,8 +58,7 @@ public void setAutoStartup(boolean autoStart) { } @Override - @Nullable - public Object getMessageListener() { + public @Nullable Object getMessageListener() { return null; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MismatchedQueueDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MismatchedQueueDeclarationTests.java index 1c6fa21fc8..4e12d003a4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MismatchedQueueDeclarationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MismatchedQueueDeclarationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,7 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -32,7 +30,8 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.env.StandardEnvironment; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueArgumentsParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueArgumentsParserTests.java index 3940e1fbee..13adfc1c5a 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueArgumentsParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueArgumentsParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Map; import org.junit.jupiter.api.Test; @@ -28,6 +26,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java index d00c887184..fc16e4dc00 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.time.Duration; import org.junit.jupiter.api.BeforeEach; @@ -34,10 +31,15 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + /** * @author Dave Syer * @author Gary Russell * @author Gunnar Hillert + * @author Artem Bilan + * * @since 1.0 * */ @@ -47,15 +49,14 @@ public final class QueueParserIntegrationTests { private DefaultListableBeanFactory beanFactory; @BeforeEach - public void setUpDefaultBeanFactory() throws Exception { + public void setUpDefaultBeanFactory() { beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(new ClassPathResource(getClass().getSimpleName() + "-context.xml", getClass())); } @Test - public void testArgumentsQueue() throws Exception { - + public void testArgumentsQueue() { Queue queue = beanFactory.getBean("arguments", Queue.class); assertThat(queue).isNotNull(); CachingConnectionFactory connectionFactory = new CachingConnectionFactory( @@ -67,9 +68,9 @@ public void testArgumentsQueue() throws Exception { assertThat(queue.getArguments().get("x-message-ttl")).isEqualTo(100L); template.convertAndSend(queue.getName(), "message"); - await().with().pollInterval(Duration.ofMillis(50)) + await().with().pollInterval(Duration.ofMillis(500)) .until(() -> rabbitAdmin.getQueueProperties("arguments") - .get(RabbitAdmin.QUEUE_MESSAGE_COUNT).equals(0)); + .get(RabbitAdmin.QUEUE_MESSAGE_COUNT).equals(0L)); connectionFactory.destroy(); RabbitAvailableCondition.getBrokerRunning().deleteQueues("arguments"); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserTests.java index 076633dd83..411345a0c5 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,6 +28,9 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java index efa50d529e..576b0cd282 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,12 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,7 +40,8 @@ import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; import org.springframework.util.ReflectionUtils; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Stephane Nicoll @@ -77,8 +76,8 @@ private void invokeListener(RabbitListenerEndpoint endpoint, Message message) th SimpleMessageListenerContainer messageListenerContainer = containerFactory.createListenerContainer(endpoint); Object listener = messageListenerContainer.getMessageListener(); - if (listener instanceof ChannelAwareMessageListener) { - ((ChannelAwareMessageListener) listener).onMessage(message, mock(Channel.class)); + if (listener instanceof ChannelAwareMessageListener awareMessageListener) { + awareMessageListener.onMessage(message, mock(Channel.class)); } else { ((MessageListener) listener).onMessage(message); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java index 9707b14942..780951c387 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.config; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.util.List; import java.util.concurrent.Executor; @@ -28,6 +24,7 @@ import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; @@ -35,6 +32,7 @@ import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; import org.springframework.scheduling.TaskScheduler; import org.springframework.transaction.PlatformTransactionManager; @@ -42,6 +40,9 @@ import org.springframework.util.backoff.BackOff; import org.springframework.util.backoff.ExponentialBackOff; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Stephane Nicoll * @author Artem Bilan @@ -70,12 +71,17 @@ public void createSimpleContainer() { SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); endpoint.setMessageListener(this.messageListener); endpoint.setQueueNames("myQueue"); + BatchingStrategy bs1 = mock(BatchingStrategy.class); + this.factory.setBatchingStrategy(bs1); + BatchingStrategy bs2 = mock(BatchingStrategy.class); + endpoint.setBatchingStrategy(bs2); SimpleMessageListenerContainer container = this.factory.createListenerContainer(endpoint); assertBasicConfig(container); assertThat(container.getMessageListener()).isEqualTo(messageListener); assertThat(container.getQueueNames()[0]).isEqualTo("myQueue"); + assertThat(TestUtils.getPropertyValue(container, "batchingStrategy")).isSameAs(bs2); } @Test @@ -89,6 +95,8 @@ public void createContainerFullConfig() { this.factory.setTaskExecutor(executor); this.factory.setTransactionManager(transactionManager); this.factory.setBatchSize(10); + BatchingStrategy bs1 = mock(BatchingStrategy.class); + this.factory.setBatchingStrategy(bs1); this.factory.setConcurrentConsumers(2); this.factory.setMaxConcurrentConsumers(5); this.factory.setStartConsumerMinInterval(2000L); @@ -105,6 +113,7 @@ public void createContainerFullConfig() { this.factory.setAfterReceivePostProcessors(afterReceivePostProcessor); this.factory.setGlobalQos(true); this.factory.setContainerCustomizer(c -> c.setShutdownTimeout(10_000)); + this.factory.setForceStop(true); assertThat(this.factory.getAdviceChain()).isEqualTo(new Advice[]{advice}); @@ -115,6 +124,7 @@ public void createContainerFullConfig() { SimpleMessageListenerContainer container = this.factory.createListenerContainer(endpoint); assertBasicConfig(container); + assertThat(TestUtils.getPropertyValue(container, "batchingStrategy")).isSameAs(bs1); DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(container); assertThat(fieldAccessor.getPropertyValue("taskExecutor")).isSameAs(executor); assertThat(fieldAccessor.getPropertyValue("transactionManager")).isSameAs(transactionManager); @@ -140,6 +150,7 @@ public void createContainerFullConfig() { assertThat(actualAfterReceivePostProcessors.size()).as("Wrong number of afterReceivePostProcessors").isEqualTo(1); assertThat(actualAfterReceivePostProcessors.get(0)).as("Wrong advice").isSameAs(afterReceivePostProcessor); assertThat(fieldAccessor.getPropertyValue("globalQos")).isEqualTo(true); + assertThat(TestUtils.getPropertyValue(container, "forceStop", Boolean.class)).isTrue(); } @Test @@ -166,6 +177,7 @@ public void createDirectContainerFullConfig() { this.direct.setMessagesPerAck(5); this.direct.setAckTimeout(3L); this.direct.setAfterReceivePostProcessors(afterReceivePostProcessor); + this.direct.setForceStop(true); assertThat(this.direct.getAdviceChain()).isEqualTo(new Advice[]{advice}); @@ -197,6 +209,7 @@ public void createDirectContainerFullConfig() { List actualAfterReceivePostProcessors = (List) fieldAccessor.getPropertyValue("afterReceivePostProcessors"); assertThat(actualAfterReceivePostProcessors.size()).as("Wrong number of afterReceivePostProcessors").isEqualTo(1); assertThat(actualAfterReceivePostProcessors.get(0)).as("Wrong afterReceivePostProcessor").isSameAs(afterReceivePostProcessor); + assertThat(TestUtils.getPropertyValue(container, "forceStop", Boolean.class)).isTrue(); } private void setBasicConfig(AbstractRabbitListenerContainerFactory factory) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java index fa592274fb..1360ebd5e8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -28,6 +26,8 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; +import static org.assertj.core.api.Assertions.assertThat; + /** * * @author Stephane Nicoll @@ -40,6 +40,8 @@ public class RabbitListenerContainerTestFactory implements RabbitListenerContain private final Map listenerContainers = new LinkedHashMap(); + private String beanName; + public List getListenerContainers() { return new ArrayList(this.listenerContainers.values()); } @@ -63,4 +65,13 @@ public MessageListenerTestContainer createListenerContainer(RabbitListenerEndpoi return container; } + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + public String getBeanName() { + return this.beanName; + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitNamespaceHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitNamespaceHandlerTests.java index 534e47e7e4..e96c9a7ebe 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitNamespaceHandlerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -35,6 +33,8 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Tomas Lukosius * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java index bf7f3a7e38..e30a18e06d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,12 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.util.Collections; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import org.aopalliance.intercept.MethodInterceptor; import org.junit.jupiter.api.Test; @@ -46,6 +43,10 @@ import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @author Artem Bilan @@ -103,7 +104,9 @@ public void testWithCustomBackOffPolicy() { .build(); assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts")).isEqualTo(5); - assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod")).isEqualTo(1000L); + assertThat(TestUtils + .getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod", Supplier.class).get()) + .isEqualTo(1000L); } @Test @@ -120,7 +123,9 @@ public void testWithCustomNewMessageIdentifier() throws Exception { .build(); assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts")).isEqualTo(5); - assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod")).isEqualTo(1000L); + assertThat(TestUtils + .getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod", Supplier.class).get()) + .isEqualTo(1000L); final AtomicInteger count = new AtomicInteger(); Foo delegate = createDelegate(interceptor, count); Message message = MessageBuilder.withBody("".getBytes()).setMessageId("foo").setRedelivered(false).build(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpointTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpointTests.java index 738b4bcf74..8e1340c282 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpointTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,6 @@ package org.springframework.amqp.rabbit.config; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.mock; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageListener; @@ -28,6 +23,10 @@ import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + /** * @author Stephane Nicoll * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/TemplateParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/TemplateParserTests.java index 7ef29168e2..be8cef0699 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/TemplateParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/TemplateParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import org.junit.jupiter.api.BeforeEach; @@ -36,6 +34,8 @@ import org.springframework.retry.RecoveryCallback; import org.springframework.retry.support.RetryTemplate; +import static org.assertj.core.api.Assertions.assertThat; + /** * * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java index 3fc935d5cb..04b470d2e7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2020 the original author or authors. + * Copyright 2010-2025 the original author or 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,48 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willCallRealMethod; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.Collections; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.amqp.AmqpResourceNotAvailableException; +import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.AddressShuffleMode; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; +import org.springframework.util.StopWatch; +import org.springframework.util.backoff.FixedBackOff; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willCallRealMethod; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Dave Syer * @author Gary Russell * @author Dmitry Dbrazhnikov * @author Artem Bilan + * @author Salk Lee */ public abstract class AbstractConnectionFactoryTests { @@ -58,11 +65,11 @@ public abstract class AbstractConnectionFactoryTests { @Test public void testWithListener() throws Exception { - - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + com.rabbitmq.client.Connection mockConnection = mock(); given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); + given(mockConnectionFactory.newConnection(any(), anyList(), anyString())).willReturn(mockConnection); final AtomicInteger called = new AtomicInteger(0); AbstractConnectionFactory connectionFactory = createConnectionFactory(mockConnectionFactory); @@ -103,7 +110,8 @@ public void onClose(Connection connection) { verify(mockConnectionFactory, times(1)).newConnection(any(ExecutorService.class), anyString()); - connectionFactory.setAddresses("foo:5672,bar:5672"); + connectionFactory.setAddresses(List.of("foo:5672", "bar:5672")); + connectionFactory.setAddressShuffleMode(AddressShuffleMode.NONE); con = connectionFactory.createConnection(); assertThat(called.get()).isEqualTo(1); captor = ArgumentCaptor.forClass(String.class); @@ -118,9 +126,8 @@ public void onClose(Connection connection) { @Test public void testWithListenerRegisteredAfterOpen() throws Exception { - - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + com.rabbitmq.client.Connection mockConnection = mock(); given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); @@ -161,10 +168,9 @@ public void onClose(Connection connection) { @Test public void testCloseInvalidConnection() throws Exception { - - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - com.rabbitmq.client.Connection mockConnection1 = mock(com.rabbitmq.client.Connection.class); - com.rabbitmq.client.Connection mockConnection2 = mock(com.rabbitmq.client.Connection.class); + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + com.rabbitmq.client.Connection mockConnection1 = mock(); + com.rabbitmq.client.Connection mockConnection2 = mock(); given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())) .willReturn(mockConnection1, mockConnection2); @@ -187,8 +193,7 @@ public void testCloseInvalidConnection() throws Exception { @Test public void testDestroyBeforeUsed() throws Exception { - - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); AbstractConnectionFactory connectionFactory = createConnectionFactory(mockConnectionFactory); connectionFactory.destroy(); @@ -198,7 +203,7 @@ public void testDestroyBeforeUsed() throws Exception { @Test public void testCreatesConnectionWithGivenFactory() { - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); willCallRealMethod().given(mockConnectionFactory).params(any(ExecutorService.class)); willCallRealMethod().given(mockConnectionFactory).setThreadFactory(any(ThreadFactory.class)); willCallRealMethod().given(mockConnectionFactory).getThreadFactory(); @@ -210,4 +215,22 @@ public void testCreatesConnectionWithGivenFactory() { assertThat(mockConnectionFactory.getThreadFactory()).isEqualTo(connectionThreadFactory); } + @Test + public void testConnectionCreatingBackOff() throws Exception { + int maxAttempts = 2; + long interval = 100L; + com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); + given(mockConnection.createChannel()).willReturn(null); + SimpleConnection simpleConnection = new SimpleConnection(mockConnection, 5, + new FixedBackOff(interval, maxAttempts).start()); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + assertThatExceptionOfType(AmqpResourceNotAvailableException.class).isThrownBy(() -> { + simpleConnection.createChannel(false); + }); + stopWatch.stop(); + assertThat(stopWatch.getTotalTimeMillis()).isGreaterThanOrEqualTo(maxAttempts * interval); + verify(mockConnection, times(maxAttempts + 1)).createChannel(); + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachePropertiesTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachePropertiesTests.java index 16c501f047..93e31077fd 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachePropertiesTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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,10 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode; @@ -31,7 +30,7 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java index 14cc4663b3..3b21dfd2e3 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; @@ -41,6 +31,8 @@ import javax.net.ServerSocketFactory; import javax.net.SocketFactory; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -67,8 +59,15 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.event.ContextClosedEvent; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Dave Syer @@ -80,8 +79,8 @@ * */ @RabbitAvailable(queues = CachingConnectionFactoryIntegrationTests.CF_INTEGRATION_TEST_QUEUE) -@LogLevels(classes = { CachingConnectionFactoryIntegrationTests.class, - CachingConnectionFactory.class }, categories = "com.rabbitmq", level = "DEBUG") +@LogLevels(classes = {CachingConnectionFactoryIntegrationTests.class, + CachingConnectionFactory.class}, categories = "com.rabbitmq", level = "DEBUG") public class CachingConnectionFactoryIntegrationTests { public static final String CF_INTEGRATION_TEST_QUEUE = "cfIntegrationTest"; @@ -269,16 +268,16 @@ public void testReceiveFromNonExistentVirtualHost() { RabbitTemplate template = new RabbitTemplate(connectionFactory); assertThatThrownBy(() -> template.receiveAndConvert("foo")) - .isInstanceOfAny( - // Wrong vhost is very unfriendly to client - the exception has no clue (just an EOF) - AmqpIOException.class, - AmqpAuthenticationException.class, - /* - * If localhost also resolves to an IPv6 address, the client will try that - * after a failure due to an invalid vHost and, if Rabbit is not listening there, - * we'll get an... - */ - AmqpConnectException.class); + .isInstanceOfAny( + // Wrong vhost is very unfriendly to client - the exception has no clue (just an EOF) + AmqpIOException.class, + AmqpAuthenticationException.class, + /* + * If localhost also resolves to an IPv6 address, the client will try that + * after a failure due to an invalid vHost and, if Rabbit is not listening there, + * we'll get an... + */ + AmqpConnectException.class); } @Test @@ -291,11 +290,11 @@ public void testSendAndReceiveFromVolatileQueueAfterImplicitRemoval() throws Exc template.convertAndSend(queue.getName(), "message"); // Force a physical close of the channel - this.connectionFactory.resetConnection(); + this.connectionFactory.stop(); // The queue was removed when the channel was closed assertThatThrownBy(() -> template.receiveAndConvert(queue.getName())) - .isInstanceOf(AmqpIOException.class); + .isInstanceOf(AmqpIOException.class); template.stop(); } @@ -315,11 +314,11 @@ public void testMixTransactionalAndNonTransactional() throws Exception { // The channel is not transactional assertThatThrownBy(() -> - template2.execute(channel -> { - // Should be an exception because the channel is not transactional - channel.txRollback(); - return null; - })).isInstanceOf(AmqpIOException.class); + template2.execute(channel -> { + // Should be an exception because the channel is not transactional + channel.txRollback(); + return null; + })).isInstanceOf(AmqpIOException.class); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java index 380994f869..e4d4a223d5 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,29 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - import java.io.IOException; import java.net.URI; import java.util.ArrayList; @@ -62,10 +39,18 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import com.rabbitmq.client.Address; +import com.rabbitmq.client.AddressResolver; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConfirmListener; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.GetResponse; +import com.rabbitmq.client.ShutdownSignalException; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.springframework.amqp.AmqpConnectException; @@ -79,13 +64,29 @@ import org.springframework.context.event.ContextClosedEvent; import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.Address; -import com.rabbitmq.client.AddressResolver; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConfirmListener; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.GetResponse; -import com.rabbitmq.client.ShutdownSignalException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * @author Mark Pollack @@ -103,6 +104,28 @@ protected AbstractConnectionFactory createConnectionFactory(ConnectionFactory co return ccf; } + @Test + void stringRepresentation() { + CachingConnectionFactory ccf = new CachingConnectionFactory("someHost", 1234); + assertThat(ccf.toString()).contains(", host=someHost, port=1234") + .doesNotContain("addresses"); + ccf.setAddresses("h1:1234,h2:1235"); + assertThat(ccf.toString()).contains(", addresses=[h1:1234, h2:1235]") + .doesNotContain("host") + .doesNotContain("port"); + ccf.setAddressResolver(() -> List.of(new Address("h3", 1236), new Address("h4", 1237))); + assertThat(ccf.toString()).contains(", addresses=[h3:1236, h4:1237]") + .doesNotContain("host") + .doesNotContain("port"); + ccf.setAddressResolver(() -> { + throw new IOException("test"); + }); + ccf.setPort(0); + assertThat(ccf.toString()).contains(", host=AddressResolver threw exception: test") + .doesNotContain("addresses") + .doesNotContain("port"); + } + @Test public void testWithConnectionFactoryDefaults() throws Exception { com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); @@ -535,10 +558,10 @@ private void testCheckoutsWithRefreshedConnectionGuts(CacheMode mode) throws Exc } @Test - public void testCheckoutLimitWithRelease() throws IOException, Exception { - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); - Channel mockChannel1 = mock(Channel.class); + public void testCheckoutLimitWithRelease() throws Exception { + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + com.rabbitmq.client.Connection mockConnection = mock(); + Channel mockChannel1 = mock(); given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); given(mockConnection.createChannel()).willReturn(mockChannel1); @@ -587,19 +610,19 @@ public void testCheckoutLimitWithRelease() throws IOException, Exception { } @Test - public void testCheckoutLimitWithPublisherConfirmsLogical() throws IOException, Exception { + public void testCheckoutLimitWithPublisherConfirmsLogical() throws Exception { testCheckoutLimitWithPublisherConfirms(false); } @Test - public void testCheckoutLimitWithPublisherConfirmsPhysical() throws IOException, Exception { + public void testCheckoutLimitWithPublisherConfirmsPhysical() throws Exception { testCheckoutLimitWithPublisherConfirms(true); } - private void testCheckoutLimitWithPublisherConfirms(boolean physicalClose) throws IOException, Exception { - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); - Channel mockChannel = mock(Channel.class); + private void testCheckoutLimitWithPublisherConfirms(boolean physicalClose) throws Exception { + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + com.rabbitmq.client.Connection mockConnection = mock(); + Channel mockChannel = mock(); given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); given(mockConnection.createChannel()).willReturn(mockChannel); @@ -631,7 +654,7 @@ private void testCheckoutLimitWithPublisherConfirms(boolean physicalClose) throw RabbitTemplate rabbitTemplate = new RabbitTemplate(ccf); if (physicalClose) { Channel channel1 = con.createChannel(false); - RabbitUtils.setPhysicalCloseRequired(channel1, physicalClose); + RabbitUtils.setPhysicalCloseRequired(channel1, true); channel1.close(); } else { @@ -670,7 +693,7 @@ private void testCheckoutLimitWithPublisherConfirms(boolean physicalClose) throw } @Test - public void testCheckoutLimitWithPublisherConfirmsLogicalAlreadyCloses() throws IOException, Exception { + public void testCheckoutLimitWithPublisherConfirmsLogicalAlreadyCloses() throws Exception { com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); Channel mockChannel = mock(Channel.class); @@ -687,7 +710,7 @@ public void testCheckoutLimitWithPublisherConfirmsLogicalAlreadyCloses() throws willAnswer(invoc -> { open.set(false); // so the logical close detects a closed delegate return null; - }).given(mockChannel).basicPublish(any(), any(), anyBoolean(), any(), any()); + }).given(mockChannel).basicPublish(any(), any(), anyBoolean(), any(), any()); CachingConnectionFactory ccf = new CachingConnectionFactory(mockConnectionFactory); ccf.setExecutor(mock(ExecutorService.class)); @@ -699,7 +722,7 @@ public void testCheckoutLimitWithPublisherConfirmsLogicalAlreadyCloses() throws rabbitTemplate.convertAndSend("foo", "bar"); open.set(true); rabbitTemplate.convertAndSend("foo", "bar"); - verify(mockChannel, times(2)).basicPublish(any(), any(), anyBoolean(), any(), any()); + verify(mockChannel, times(2)).basicPublish(any(), any(), anyBoolean(), any(), any()); } @Test @@ -1277,7 +1300,6 @@ public void onClose(Connection connection) { verify(mockConnections.get(3)).close(30000); } - @Test public void testWithConnectionFactoryCachedConnectionAndChannels() throws Exception { com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); @@ -1587,7 +1609,7 @@ private void testConsumerChannelPhysicallyClosedWhenNotIsOpenGuts(boolean confir Channel channel = con.createChannel(false); RabbitUtils.setPhysicalCloseRequired(channel, true); - given(mockChannel.isOpen()).willReturn(false); + given(mockChannel.isOpen()).willReturn(true); final CountDownLatch physicalCloseLatch = new CountDownLatch(1); willAnswer(i -> { physicalCloseLatch.countDown(); @@ -1621,6 +1643,8 @@ private void verifyChannelIs(Channel mockChannel, Channel channel) { @Test public void setAddressesEmpty() throws Exception { ConnectionFactory mock = mock(com.rabbitmq.client.ConnectionFactory.class); + given(mock.newConnection(any(ExecutorService.class), anyString())) + .willReturn(mock(com.rabbitmq.client.Connection.class)); CachingConnectionFactory ccf = new CachingConnectionFactory(mock); ccf.setExecutor(mock(ExecutorService.class)); ccf.setHost("abc"); @@ -1640,6 +1664,8 @@ public void setAddressesEmpty() throws Exception { @Test public void setAddressesOneHost() throws Exception { ConnectionFactory mock = mock(com.rabbitmq.client.ConnectionFactory.class); + given(mock.newConnection(any(), anyList(), anyString())) + .willReturn(mock(com.rabbitmq.client.Connection.class)); CachingConnectionFactory ccf = new CachingConnectionFactory(mock); ccf.setAddresses("mq1"); ccf.createConnection(); @@ -1651,15 +1677,19 @@ public void setAddressesOneHost() throws Exception { @Test public void setAddressesTwoHosts() throws Exception { - ConnectionFactory mock = mock(com.rabbitmq.client.ConnectionFactory.class); + ConnectionFactory mock = mock(); willReturn(true).given(mock).isAutomaticRecoveryEnabled(); + willReturn(mock(com.rabbitmq.client.Connection.class)).given(mock).newConnection(any(), anyList(), anyString()); CachingConnectionFactory ccf = new CachingConnectionFactory(mock); ccf.setAddresses("mq1,mq2"); ccf.createConnection(); verify(mock).isAutomaticRecoveryEnabled(); verify(mock).setAutomaticRecoveryEnabled(false); - verify(mock).newConnection(isNull(), - eq(Arrays.asList(new Address("mq1"), new Address("mq2"))), anyString()); + verify(mock).newConnection( + isNull(), + argThat((ArgumentMatcher>) a -> a.size() == 2 + && a.contains(new Address("mq1")) && a.contains(new Address("mq2"))), + anyString()); verifyNoMoreInteractions(mock); } @@ -1667,7 +1697,9 @@ public void setAddressesTwoHosts() throws Exception { public void setUri() throws Exception { URI uri = new URI("amqp://localhost:1234/%2f"); - ConnectionFactory mock = mock(com.rabbitmq.client.ConnectionFactory.class); + ConnectionFactory mock = mock(); + given(mock.newConnection(any(ExecutorService.class), anyString())) + .willReturn(mock(com.rabbitmq.client.Connection.class)); CachingConnectionFactory ccf = new CachingConnectionFactory(mock); ccf.setExecutor(mock(ExecutorService.class)); @@ -1763,7 +1795,7 @@ public void testOrderlyShutDown() throws Exception { AtomicBoolean rejected = new AtomicBoolean(true); CountDownLatch closeLatch = new CountDownLatch(1); ccf.setPublisherChannelFactory((channel, exec) -> { - executor.set(spy(exec)); + executor.set(exec); return pcc; }); willAnswer(invoc -> { @@ -1829,12 +1861,12 @@ public void testFirstConnectionDoesntWait() throws IOException, TimeoutException @SuppressWarnings("unchecked") @Test public void testShuffleRandom() throws IOException, TimeoutException { - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + com.rabbitmq.client.Connection mockConnection = mock(); Channel mockChannel = mock(Channel.class); - given(mockConnectionFactory.newConnection((ExecutorService) isNull(), any(List.class), anyString())) - .willReturn(mockConnection); + given(mockConnectionFactory.newConnection(any(), anyList(), anyString())) + .willReturn(mockConnection); given(mockConnection.createChannel()).willReturn(mockChannel); given(mockChannel.isOpen()).willReturn(true); given(mockConnection.isOpen()).willReturn(true); @@ -1848,11 +1880,11 @@ public void testShuffleRandom() throws IOException, TimeoutException { ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); verify(mockConnectionFactory, times(100)).newConnection(isNull(), captor.capture(), anyString()); List firstAddress = captor.getAllValues() - .stream() - .map(addresses -> addresses.get(0).getHost()) - .distinct() - .sorted() - .collect(Collectors.toList()); + .stream() + .map(addresses -> addresses.get(0).getHost()) + .distinct() + .sorted() + .collect(Collectors.toList()); assertThat(firstAddress).containsExactly("host1", "host2", "host3"); } @@ -1863,8 +1895,8 @@ public void testShuffleInOrder() throws IOException, TimeoutException { com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); Channel mockChannel = mock(Channel.class); - given(mockConnectionFactory.newConnection((ExecutorService) isNull(), any(List.class), anyString())) - .willReturn(mockConnection); + given(mockConnectionFactory.newConnection(isNull(), anyList(), anyString())) + .willReturn(mockConnection); given(mockConnection.createChannel()).willReturn(mockChannel); given(mockChannel.isOpen()).willReturn(true); given(mockConnection.isOpen()).willReturn(true); @@ -1878,17 +1910,17 @@ public void testShuffleInOrder() throws IOException, TimeoutException { ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); verify(mockConnectionFactory, times(3)).newConnection(isNull(), captor.capture(), anyString()); List connectAddresses = captor.getAllValues() - .stream() - .map(addresses -> addresses.get(0).getHost()) - .collect(Collectors.toList()); + .stream() + .map(addresses -> addresses.get(0).getHost()) + .collect(Collectors.toList()); assertThat(connectAddresses).containsExactly("host1", "host2", "host3"); } @Test void testResolver() throws Exception { - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); - Channel mockChannel = mock(Channel.class); + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + com.rabbitmq.client.Connection mockConnection = mock(); + Channel mockChannel = mock(); AddressResolver resolver = () -> Collections.singletonList(Address.parseAddress("foo:5672")); given(mockConnectionFactory.newConnection(any(ExecutorService.class), eq(resolver), anyString())) @@ -1907,4 +1939,63 @@ void testResolver() throws Exception { verify(mockConnectionFactory).newConnection(any(ExecutorService.class), eq(resolver), anyString()); } + @Test + void nullShutdownCause() { + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + AbstractConnectionFactory cf = createConnectionFactory(mockConnectionFactory); + AtomicBoolean connShutDown = new AtomicBoolean(); + cf.addConnectionListener(new ConnectionListener() { + + @Override + public void onCreate(Connection connection) { + } + + @Override + public void onShutDown(ShutdownSignalException signal) { + connShutDown.set(true); + } + + }); + AtomicBoolean chanShutDown = new AtomicBoolean(); + cf.addChannelListener(new ChannelListener() { + + @Override + public void onCreate(Channel channel, boolean transactional) { + } + + @Override + public void onShutDown(ShutdownSignalException signal) { + chanShutDown.set(true); + } + + }); + cf.shutdownCompleted(new ShutdownSignalException(false, false, null, chanShutDown)); + assertThat(connShutDown.get()).isTrue(); + assertThat(chanShutDown.get()).isFalse(); + } + + @Test + void isPublisherConfirmsHandlesSimple() { + CachingConnectionFactory ccf = new CachingConnectionFactory("someHost", 1234); + ccf.setPublisherConfirmType(ConfirmType.SIMPLE); + + assertThat(ccf.isPublisherConfirms()).isFalse(); + } + + @Test + void isPublisherConfirmsHandlesCorrelated() { + CachingConnectionFactory ccf = new CachingConnectionFactory("someHost", 1234); + ccf.setPublisherConfirmType(ConfirmType.CORRELATED); + + assertThat(ccf.isPublisherConfirms()).isTrue(); + } + + @Test + void isPublisherConfirmsHandlesNone() { + CachingConnectionFactory ccf = new CachingConnectionFactory("someHost", 1234); + ccf.setPublisherConfirmType(ConfirmType.NONE); + + assertThat(ccf.isPublisherConfirms()).isFalse(); + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ClientRecoveryCompatibilityTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ClientRecoveryCompatibilityTests.java index 629bfa4bfe..bb04708c26 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ClientRecoveryCompatibilityTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ClientRecoveryCompatibilityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author or 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,12 @@ package org.springframework.amqp.rabbit.connection; +import java.util.concurrent.ExecutorService; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; +import org.junit.jupiter.api.Test; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; @@ -26,13 +32,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import java.util.concurrent.ExecutorService; - -import org.junit.jupiter.api.Test; - -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; - /** * @author Gary Russell * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapperTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapperTests.java index 26637881f3..20cf120595 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapperTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,16 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.util.concurrent.Callable; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + /** * @author Wander Costa */ diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryLifecycleTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryLifecycleTests.java index 1b8c67aa11..2ed613e970 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryLifecycleTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryLifecycleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or 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.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.BlockedListener; +import com.rabbitmq.client.impl.AMQCommand; +import com.rabbitmq.client.impl.AMQConnection; +import com.rabbitmq.client.impl.AMQImpl; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpApplicationContextClosedException; @@ -38,10 +39,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.rabbitmq.client.BlockedListener; -import com.rabbitmq.client.impl.AMQCommand; -import com.rabbitmq.client.impl.AMQConnection; -import com.rabbitmq.client.impl.AMQImpl; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtilsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtilsTests.java index a59ca09443..b2de758b2c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtilsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import org.junit.jupiter.api.Test; import org.springframework.transaction.support.TransactionSynchronizationManager; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Gary Russell * @since 1.7.1 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnnectionListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java similarity index 94% rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnnectionListenerTests.java rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java index 03638c6a15..8d94733dd1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnnectionListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or 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,22 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpIOException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Gary Russell + * @author DongMin Park * @since 2.2.17 * */ -public class ConnnectionListenerTests { +public class ConnectionListenerTests { @Test void cantConnectCCF() { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java new file mode 100644 index 0000000000..9ce3267893 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.connection; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author Artem Bilan + * + * @since 3.0.11 + * + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +@DirtiesContext +public class ConsumerConnectionRecoveryTests { + + @Container + static final RabbitMQContainer RABBIT_MQ_CONTAINER = + new RabbitMQContainer(DockerImageName.parse("rabbitmq")); + + @Test + void verifyThatChannelPermitsAreReleaseOnReconnect(@Autowired TestConfiguration application) + throws InterruptedException { + + application.rabbitTemplate().convertAndSend("testQueue", "test data #1"); + + assertThat(application.received.poll(20, TimeUnit.SECONDS)).isEqualTo("test data #1"); + + RABBIT_MQ_CONTAINER.stop(); + RABBIT_MQ_CONTAINER.start(); + + application.connectionFactory().setPort(RABBIT_MQ_CONTAINER.getAmqpPort()); + application.publisherConnectionFactory().setPort(RABBIT_MQ_CONTAINER.getAmqpPort()); + + application.rabbitTemplate().convertAndSend("testQueue", "test data #2"); + + assertThat(application.received.poll(30, TimeUnit.SECONDS)).isEqualTo("test data #2"); + } + + @Configuration + @EnableRabbit + public static class TestConfiguration { + + @Bean + CachingConnectionFactory connectionFactory() { + CachingConnectionFactory connectionFactory = + new CachingConnectionFactory("localhost", RABBIT_MQ_CONTAINER.getAmqpPort()); + connectionFactory.setChannelCacheSize(1); + connectionFactory.setChannelCheckoutTimeout(2000); + return connectionFactory; + } + + @Bean + CachingConnectionFactory publisherConnectionFactory() { + CachingConnectionFactory connectionFactory = + new CachingConnectionFactory("localhost", RABBIT_MQ_CONTAINER.getAmqpPort()); + connectionFactory.setChannelCacheSize(1); + connectionFactory.setChannelCheckoutTimeout(2000); + return connectionFactory; + } + + @Bean + RabbitTemplate rabbitTemplate() { + return new RabbitTemplate(publisherConnectionFactory()); + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + return factory; + } + + @Bean + RabbitAdmin rabbitAdmin() { + return new RabbitAdmin(publisherConnectionFactory()); + } + + BlockingQueue received = new LinkedBlockingQueue<>(); + + @RabbitListener(queuesToDeclare = @Queue("testQueue")) + void consume(String payload) { + this.received.add(payload); + } + + } + +} + diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java index 117c7f4765..691d4d9c26 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,40 +16,63 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.UUID; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.amqp.rabbit.junit.BrokerRunningSupport; import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * * @author Gary Russell */ -@RabbitAvailable(management = true) -public class LocalizedQueueConnectionFactoryIntegrationTests { +@RabbitAvailable(management = true, queues = "local") +public class LocalizedQueueConnectionFactoryIntegrationTests extends AbstractTestContainerTests { private LocalizedQueueConnectionFactory lqcf; private CachingConnectionFactory defaultConnectionFactory; + private CachingConnectionFactory testContainerFactory; + + private RabbitAdmin defaultAdmin; + + private RabbitAdmin testContainerAdmin; + @BeforeEach public void setup() { this.defaultConnectionFactory = new CachingConnectionFactory("localhost"); - String[] addresses = new String[] { "localhost:9999", "localhost:5672" }; - String[] adminUris = new String[] { "http://localhost:15672", "http://localhost:15672" }; - String[] nodes = new String[] { "foo@bar", "rabbit@localhost" }; + this.defaultAdmin = new RabbitAdmin(this.defaultConnectionFactory); + this.testContainerFactory = new CachingConnectionFactory("localhost", amqpPort()); + this.testContainerAdmin = new RabbitAdmin(this.testContainerFactory); + BrokerRunningSupport brokerRunning = RabbitAvailableCondition.getBrokerRunning(); + + String[] addresses = new String[] {brokerRunning.getHostName() + ":" + brokerRunning.getPort(), + RABBITMQ.getHost() + ":" + RABBITMQ.getAmqpPort()}; + String[] adminUris = new String[] {brokerRunning.getAdminUri(), RABBITMQ.getHttpUrl()}; + String[] nodes = new String[] {findLocalNode(), findTcNode()}; String vhost = "/"; - String username = "guest"; - String password = "guest"; + String username = brokerRunning.getAdminUser(); + String password = brokerRunning.getAdminPassword(); this.lqcf = new LocalizedQueueConnectionFactory(defaultConnectionFactory, addresses, adminUris, nodes, vhost, username, password, false, null); } @@ -58,18 +81,98 @@ public void setup() { public void tearDown() { this.lqcf.destroy(); this.defaultConnectionFactory.destroy(); + this.testContainerFactory.destroy(); + } + + @Test + public void testFindCorrectConnection() throws Exception { + AnonymousQueue externalQueue = new AnonymousQueue(); + AnonymousQueue tcQueue = new AnonymousQueue(); + this.defaultAdmin.declareQueue(externalQueue); + this.testContainerAdmin.declareQueue(tcQueue); + ConnectionFactory cf = this.lqcf + .getTargetConnectionFactory("[" + externalQueue.getName() + "]"); + assertThat(cf).isNotSameAs(this.defaultConnectionFactory); + assertThat(this.defaultAdmin.getQueueProperties(externalQueue.getName())).isNotNull(); + cf = this.lqcf.getTargetConnectionFactory("[" + tcQueue.getName() + "]"); + assertThat(cf).isNotSameAs(this.defaultConnectionFactory); + assertThat(this.testContainerAdmin.getQueueProperties(tcQueue.getName())).isNotNull(); + this.defaultAdmin.deleteQueue(externalQueue.getName()); + this.testContainerAdmin.deleteQueue(tcQueue.getName()); } @Test - public void testConnect() throws Exception { - RabbitAdmin admin = new RabbitAdmin(this.lqcf); - Queue queue = new Queue(UUID.randomUUID().toString(), false, false, true); - admin.declareQueue(queue); - ConnectionFactory targetConnectionFactory = this.lqcf.getTargetConnectionFactory("[" + queue.getName() + "]"); - RabbitTemplate template = new RabbitTemplate(targetConnectionFactory); - template.convertAndSend("", queue.getName(), "foo"); - assertThat(template.receiveAndConvert(queue.getName())).isEqualTo("foo"); - admin.deleteQueue(queue.getName()); + void findLocal() { + ConnectionFactory defaultCf = mock(ConnectionFactory.class); + BrokerRunningSupport brokerRunning = RabbitAvailableCondition.getBrokerRunning(); + LocalizedQueueConnectionFactory lqcf = new LocalizedQueueConnectionFactory(defaultCf, + Map.of(findLocalNode(), brokerRunning.getHostName() + ":" + brokerRunning.getPort()), + new String[] {brokerRunning.getAdminUri()}, + "/", brokerRunning.getAdminUser(), brokerRunning.getAdminPassword(), false, null); + ConnectionFactory cf = lqcf.getTargetConnectionFactory("[local]"); + RabbitAdmin admin = new RabbitAdmin(cf); + assertThat(admin.getQueueProperties("local")).isNotNull(); + lqcf.setNodeLocator(new RestTemplateNodeLocator()); + ConnectionFactory cf2 = lqcf.getTargetConnectionFactory("[local]"); + assertThat(cf2).isSameAs(cf); + lqcf.destroy(); + } + + private String findTcNode() { + AnonymousQueue queue = new AnonymousQueue(); + this.testContainerAdmin.declareQueue(queue); + URI uri; + try { + uri = new URI(restUri()) + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + + queue.getName()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + WebClient client = WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(RABBITMQ.getAdminUsername(), + RABBITMQ.getAdminPassword())) + .build(); + Map queueInfo = client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }) + .block(Duration.ofSeconds(10)); + this.testContainerAdmin.deleteQueue(queue.getName()); + return (String) queueInfo.get("node"); + } + + private String findLocalNode() { + AnonymousQueue queue = new AnonymousQueue(); + this.defaultAdmin.declareQueue(queue); + URI uri; + BrokerRunningSupport brokerRunning = RabbitAvailableCondition.getBrokerRunning(); + try { + uri = new URI(brokerRunning.getAdminUri()) + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + + queue.getName()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + WebClient client = WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(brokerRunning.getAdminUser(), + brokerRunning.getAdminPassword())) + .build(); + Map queueInfo = client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }) + .block(Duration.ofSeconds(10)); + this.defaultAdmin.deleteQueue(queue.getName()); + return (String) queueInfo.get("node"); } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java index e7b55e55b5..b746628925 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,20 +24,36 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.internal.stubbing.answers.CallsRealMethods; +import reactor.core.publisher.Mono; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.test.web.reactive.server.HttpHandlerConnector; +import org.springframework.web.reactive.function.client.WebClient; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; - +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -58,27 +61,27 @@ */ public class LocalizedQueueConnectionFactoryTests { - private final Map channels = new HashMap(); + private final Map channels = new HashMap<>(); - private final Map consumers = new HashMap(); + private final Map consumers = new HashMap<>(); - private final Map consumerTags = new HashMap(); + private final Map consumerTags = new HashMap<>(); @Test public void testFailOver() throws Exception { ConnectionFactory defaultConnectionFactory = mockCF("localhost:1234", null); String rabbit1 = "localhost:1235"; String rabbit2 = "localhost:1236"; - String[] addresses = new String[]{rabbit1, rabbit2}; - String[] adminUris = new String[]{"http://localhost:11235", "http://localhost:11236"}; - String[] nodes = new String[]{"rabbit@foo", "rabbit@bar"}; + String[] addresses = new String[] {rabbit1, rabbit2}; + String[] adminUris = new String[] {"http://localhost:11235", "http://localhost:11236"}; + String[] nodes = new String[] {"rabbit@foo", "rabbit@bar"}; String vhost = "/"; String username = "guest"; String password = "guest"; final AtomicBoolean firstServer = new AtomicBoolean(true); - final Client client1 = doCreateClient(adminUris[0], username, password, nodes[0]); - final Client client2 = doCreateClient(adminUris[1], username, password, nodes[1]); - final Map mockCFs = new HashMap(); + final WebClient client1 = doCreateClient(adminUris[0], username, password, nodes[0]); + final WebClient client2 = doCreateClient(adminUris[1], username, password, nodes[1]); + final Map mockCFs = new HashMap<>(); CountDownLatch latch1 = new CountDownLatch(1); CountDownLatch latch2 = new CountDownLatch(1); mockCFs.put(rabbit1, mockCF(rabbit1, latch1)); @@ -86,17 +89,20 @@ public void testFailOver() throws Exception { LocalizedQueueConnectionFactory lqcf = new LocalizedQueueConnectionFactory(defaultConnectionFactory, addresses, adminUris, nodes, vhost, username, password, false, null) { - @Override - protected Client createClient(String adminUri, String username, String password) { - return firstServer.get() ? client1 : client2; - } - @Override protected ConnectionFactory createConnectionFactory(String address, String node) { return mockCFs.get(address); } }; + lqcf.setNodeLocator(new WebFluxNodeLocator() { + + @Override + public WebClient createClient(String username, String password) { + return firstServer.get() ? client1 : client2; + } + + }); Map nodeAddress = TestUtils.getPropertyValue(lqcf, "nodeToAddress", Map.class); assertThat(nodeAddress.get("rabbit@foo")).isEqualTo(rabbit1); assertThat(nodeAddress.get("rabbit@bar")).isEqualTo(rabbit2); @@ -108,6 +114,7 @@ protected ConnectionFactory createConnectionFactory(String address, String node) willAnswer(new CallsRealMethods()).given(logger).debug(anyString()); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(lqcf); + container.setReceiveTimeout(10); container.setQueueNames("q"); container.afterPropertiesSet(); container.start(); @@ -144,12 +151,20 @@ private boolean assertLog(List logRows, String expected) { return false; } - private Client doCreateClient(String uri, String username, String password, String node) { - Client client = mock(Client.class); - QueueInfo queueInfo = new QueueInfo(); - queueInfo.setNode(node); - given(client.getQueue("/", "q")).willReturn(queueInfo); - return client; + private WebClient doCreateClient(String uri, String username, String password, String node) { + ClientHttpConnector httpConnector = + new HttpHandlerConnector((request, response) -> { + response.setStatusCode(HttpStatus.OK); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + Mono json = Mono + .just(response.bufferFactory().wrap(("{\"node\":\"" + node + "\"}").getBytes())); + return response.writeWith(json) + .then(Mono.defer(response::setComplete)); + }); + + return WebClient.builder() + .clientConnector(httpConnector) + .build(); } @Test @@ -157,9 +172,9 @@ public void test2Queues() throws Exception { try { String rabbit1 = "localhost:1235"; String rabbit2 = "localhost:1236"; - String[] addresses = new String[]{rabbit1, rabbit2}; - String[] adminUris = new String[]{"http://localhost:11235", "http://localhost:11236"}; - String[] nodes = new String[]{"rabbit@foo", "rabbit@bar"}; + String[] addresses = new String[] {rabbit1, rabbit2}; + String[] adminUris = new String[] {"http://localhost:11235", "http://localhost:11236"}; + String[] nodes = new String[] {"rabbit@foo", "rabbit@bar"}; String vhost = "/"; String username = "guest"; String password = "guest"; @@ -182,8 +197,8 @@ private ConnectionFactory mockCF(final String address, final CountDownLatch latc given(channel.isOpen()).willReturn(true, false); willAnswer(invocation -> { String tag = UUID.randomUUID().toString(); - consumers.put(address, invocation.getArgument(6)); - consumerTags.put(address, tag); + this.consumers.put(address, invocation.getArgument(6)); + this.consumerTags.put(address, tag); if (latch != null) { latch.countDown(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java new file mode 100644 index 0000000000..448d7a582c --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.connection; + +import java.net.URISyntaxException; +import java.util.Map; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Gary Russell + * @author Artem Bilan + * + * @since 3.0 + * + */ +public class NodeLocatorTests { + + @Test + @DisplayName("don't exit early when node to address missing") + void missingNode() throws URISyntaxException { + + NodeLocator nodeLocator = spy(new NodeLocator() { + + @Override + public Object createClient(String userName, String password) { + return new Object(); + } + + @Override + public Map restCall(Object client, String baseUri, String vhost, String queue) { + if (baseUri.contains("foo")) { + return Map.of("node", "c@d"); + } + else { + return Map.of("node", "a@b"); + } + } + + }); + ConnectionFactory factory = nodeLocator.locate(new String[] {"http://foo", "http://bar"}, + Map.of("a@b", "baz"), "/", "q", "guest", "guest", (q, n, u) -> null); + verify(nodeLocator, times(2)).restCall(any(), any(), any(), any()); + } + + @Test + @DisplayName("rest returned null") + void nullInfo() throws URISyntaxException { + + NodeLocator nodeLocator = spy(new NodeLocator() { + + @Override + public Object createClient(String userName, String password) { + return new Object(); + } + + @Override + @Nullable + public Map restCall(Object client, String baseUri, String vhost, String queue) { + + if (baseUri.contains("foo")) { + return null; + } + else { + return Map.of("node", "a@b"); + } + } + + }); + ConnectionFactory factory = nodeLocator.locate(new String[] {"http://foo", "http://bar"}, + Map.of("a@b", "baz"), "/", "q", "guest", "guest", (q, n, u) -> null); + verify(nodeLocator, times(2)).restCall(any(), any(), any(), any()); + } + + @Test + @DisplayName("queue not found") + void notFound() throws URISyntaxException { + + NodeLocator nodeLocator = spy(new NodeLocator() { + + @Override + public Object createClient(String userName, String password) { + return new Object(); + } + + @Override + @Nullable + public Map restCall(Object client, String baseUri, String vhost, String queue) { + return null; + } + + }); + ConnectionFactory factory = nodeLocator.locate(new String[] {"http://foo", "http://bar"}, + Map.of("a@b", "baz"), "/", "q", "guest", "guest", (q, n, u) -> null); + verify(nodeLocator, times(2)).restCall(any(), any(), any(), any()); + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java index 469f086f05..d8a799fb51 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 the original author or 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,16 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; - +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; +import org.apache.commons.pool2.impl.GenericObjectPool; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -31,12 +37,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell + * @author Leonardo Ferreira * @since 2.3 * */ @@ -97,6 +104,169 @@ void queueDeclared(@Autowired RabbitAdmin admin, @Autowired Config config, assertThat(config.closed).isTrue(); } + @Test + void copyConfigsToPublisherConnectionFactory() { + PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(new ConnectionFactory()); + AtomicInteger txConfiged = new AtomicInteger(); + AtomicInteger nonTxConfiged = new AtomicInteger(); + pcf.setPoolConfigurer((pool, tx) -> { + if (tx) { + txConfiged.incrementAndGet(); + } + else { + nonTxConfiged.incrementAndGet(); + } + }); + + createAndCloseConnectionChannelTxAndChannelNonTx(pcf); + + final org.springframework.amqp.rabbit.connection.ConnectionFactory publisherConnectionFactory = pcf + .getPublisherConnectionFactory(); + assertThat(publisherConnectionFactory).isNotNull(); + + createAndCloseConnectionChannelTxAndChannelNonTx(publisherConnectionFactory); + + assertThat(txConfiged.get()).isEqualTo(2); + assertThat(nonTxConfiged.get()).isEqualTo(2); + + final Object listenerPoolConfigurer = ReflectionTestUtils.getField(pcf, "poolConfigurer"); + final Object publisherPoolConfigurer = ReflectionTestUtils.getField(publisherConnectionFactory, + "poolConfigurer"); + + assertThat(listenerPoolConfigurer) + .isSameAs(publisherPoolConfigurer); + + pcf.destroy(); + } + + @Test + void copyConfigsToPublisherConnectionFactoryWhenUsingCustomPublisherFactory() { + PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(new ConnectionFactory()); + AtomicBoolean listenerTxConfiged = new AtomicBoolean(); + AtomicBoolean listenerNonTxConfiged = new AtomicBoolean(); + pcf.setPoolConfigurer((pool, tx) -> { + if (tx) { + listenerTxConfiged.set(true); + } + else { + listenerNonTxConfiged.set(true); + } + }); + + final PooledChannelConnectionFactory publisherConnectionFactory = new PooledChannelConnectionFactory( + new ConnectionFactory()); + + AtomicBoolean publisherTxConfiged = new AtomicBoolean(); + AtomicBoolean publisherNonTxConfiged = new AtomicBoolean(); + publisherConnectionFactory.setPoolConfigurer((pool, tx) -> { + if (tx) { + publisherTxConfiged.set(true); + } + else { + publisherNonTxConfiged.set(true); + } + }); + + pcf.setPublisherConnectionFactory(publisherConnectionFactory); + + assertThat(pcf.getPublisherConnectionFactory()).isSameAs(publisherConnectionFactory); + + createAndCloseConnectionChannelTxAndChannelNonTx(pcf); + + assertThat(listenerTxConfiged.get()).isEqualTo(true); + assertThat(listenerNonTxConfiged.get()).isEqualTo(true); + + final Object listenerPoolConfigurer = ReflectionTestUtils.getField(pcf, "poolConfigurer"); + final Object publisherPoolConfigurer = ReflectionTestUtils.getField(publisherConnectionFactory, + "poolConfigurer"); + + assertThat(listenerPoolConfigurer) + .isNotSameAs(publisherPoolConfigurer); + + createAndCloseConnectionChannelTxAndChannelNonTx(publisherConnectionFactory); + + assertThat(publisherTxConfiged.get()).isEqualTo(true); + assertThat(publisherNonTxConfiged.get()).isEqualTo(true); + + pcf.destroy(); + } + + private void createAndCloseConnectionChannelTxAndChannelNonTx( + org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory) { + + Connection connection = connectionFactory.createConnection(); + Channel nonTxChannel = connection.createChannel(false); + Channel txChannel = connection.createChannel(true); + + RabbitUtils.closeChannel(nonTxChannel); + RabbitUtils.closeChannel(txChannel); + connection.close(); + } + + @Test + public void evictShouldCloseAllUnneededChannelsWithoutErrors() throws Exception { + PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(new ConnectionFactory()); + AtomicReference> channelsReference = new AtomicReference<>(); + AtomicReference> txChannelsReference = new AtomicReference<>(); + AtomicInteger swallowedExceptionsCount = new AtomicInteger(); + pcf.setPoolConfigurer((pool, tx) -> { + if (tx) { + channelsReference.set(pool); + } + else { + txChannelsReference.set(pool); + } + + pool.setEvictionPolicy((ec, u, idleCount) -> idleCount > ec.getMinIdle()); + pool.setSwallowedExceptionListener(ex -> swallowedExceptionsCount.incrementAndGet()); + pool.setNumTestsPerEvictionRun(5); + + pool.setMinIdle(1); + pool.setMaxIdle(5); + }); + + createAndCloseFiveChannelTxAndChannelNonTx(pcf); + + final GenericObjectPool channels = channelsReference.get(); + channels.evict(); + + assertThat(channels.getNumIdle()) + .isEqualTo(1); + assertThat(channels.getDestroyedByEvictorCount()) + .isEqualTo(4); + + final GenericObjectPool txChannels = txChannelsReference.get(); + txChannels.evict(); + assertThat(txChannels.getNumIdle()) + .isEqualTo(1); + assertThat(txChannels.getDestroyedByEvictorCount()) + .isEqualTo(4); + + assertThat(swallowedExceptionsCount.get()) + .isZero(); + } + + private void createAndCloseFiveChannelTxAndChannelNonTx( + org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory) { + int channelAmount = 5; + Connection connection = connectionFactory.createConnection(); + + List channels = new ArrayList<>(channelAmount); + List txChannels = new ArrayList<>(channelAmount); + + for (int i = 0; i < channelAmount; i++) { + channels.add(connection.createChannel(false)); + txChannels.add(connection.createChannel(true)); + } + + for (int i = 0; i < channelAmount; i++) { + RabbitUtils.closeChannel(channels.get(i)); + RabbitUtils.closeChannel(txChannels.get(i)); + } + + connection.close(); + } + @Configuration public static class Config { @@ -104,6 +274,7 @@ public static class Config { boolean closed; + @Nullable Connection connection; boolean channelCreated; @@ -115,27 +286,20 @@ PooledChannelConnectionFactory pccf() { pccf.addConnectionListener(new ConnectionListener() { @Override - public void onCreate(Connection connection) { + public void onCreate(@Nullable Connection connection) { Config.this.connection = connection; Config.this.created = true; } @Override public void onClose(Connection connection) { - if (Config.this.connection.equals(connection)) { + if (connection.equals(Config.this.connection)) { Config.this.closed = true; } } }); - pccf.addChannelListener(new ChannelListener() { - - @Override - public void onCreate(Channel channel, boolean transactional) { - Config.this.channelCreated = true; - } - - }); + pccf.addChannelListener((channel, transactional) -> Config.this.channelCreated = true); return pccf; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java index e738a59476..743f5f95cf 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; - import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -38,6 +31,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.IntStream; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.LongString; +import com.rabbitmq.client.Method; +import com.rabbitmq.client.Return; +import com.rabbitmq.client.ShutdownListener; +import com.rabbitmq.client.ShutdownSignalException; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; @@ -48,13 +48,12 @@ import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.core.task.SimpleAsyncTaskExecutor; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.LongString; -import com.rabbitmq.client.Method; -import com.rabbitmq.client.Return; -import com.rabbitmq.client.ShutdownListener; -import com.rabbitmq.client.ShutdownSignalException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; /** * @author Gary Russell @@ -64,11 +63,10 @@ @RabbitAvailable public class PublisherCallbackChannelTests { - @SuppressWarnings("deprecation") @Test void correlationData() { CorrelationData cd = new CorrelationData(); - assertThat(cd.getReturnedMessage()).isNull(); + assertThat(cd.getReturned()).isNull(); } @Test @@ -184,7 +182,7 @@ void confirmAlwaysAfterReturn() throws InterruptedException { assertThat(listener.calls).containsExactly("return", "confirm", "return", "confirm"); } - private static class TheListener implements Listener { + private static final class TheListener implements Listener { private final UUID uuid = UUID.randomUUID(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RabbitReconnectProblemTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RabbitReconnectProblemTests.java index a1dbbb7b51..3c9cde97d1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RabbitReconnectProblemTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RabbitReconnectProblemTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; - +import java.io.PrintStream; import java.util.Map; import java.util.concurrent.Semaphore; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -36,7 +36,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Lars Hvile @@ -48,6 +48,8 @@ @Disabled("Requires user interaction") public class RabbitReconnectProblemTests { + private static final PrintStream SOUT = System.out; + @Autowired CachingConnectionFactory connFactory; @@ -67,7 +69,7 @@ public void setup() { @Test public void surviveAReconnect() throws Exception { checkIt(0); - System .out .println("Restart RabbitMQ & press any key..."); + SOUT.println("Restart RabbitMQ & press any key..."); System.in.read(); for (int i = 1; i < 10; i++) { @@ -79,14 +81,14 @@ public void surviveAReconnect() throws Exception { .iterator() .next()) .availablePermits(); - System .out .println("Permits after test: " + availablePermits); + SOUT.println("Permits after test: " + availablePermits); assertThat(availablePermits).isEqualTo(2); } void checkIt(int counter) { - System .out .println("\n#" + counter); + SOUT.println("\n#" + counter); template.receive(myQueue.getName()); - System .out .println("OK"); + SOUT.println("OK"); } @Configuration @@ -113,5 +115,7 @@ AmqpAdmin rabbitAdmin() throws Exception { AmqpTemplate rabbitTemplate() throws Exception { return new RabbitTemplate(connectionFactory()); } + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java index fc5ea11f8a..b190a856cf 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -36,7 +25,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -45,12 +37,24 @@ import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Artem Bilan * @author Josh Chappelle * @author Gary Russell + * @author Leonardo Ferreira + * @author Christian Tzolov * @since 1.3 */ public class RoutingConnectionFactoryTests { @@ -165,7 +169,7 @@ protected Object determineCurrentLookupKey() { public void testAbstractRoutingConnectionFactoryWithListenerContainer() { ConnectionFactory connectionFactory1 = mock(ConnectionFactory.class); ConnectionFactory connectionFactory2 = mock(ConnectionFactory.class); - Map factories = new HashMap(2); + Map factories = new HashMap<>(2); factories.put("[baz]", connectionFactory1); factories.put("[foo,bar]", connectionFactory2); ConnectionFactory defaultConnectionFactory = mock(ConnectionFactory.class); @@ -176,6 +180,7 @@ public void testAbstractRoutingConnectionFactoryWithListenerContainer() { connectionFactory.setTargetConnectionFactories(factories); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setReceiveTimeout(10); container.setQueueNames("foo", "bar"); container.afterPropertiesSet(); container.start(); @@ -222,11 +227,18 @@ public void testWithSMLCAndConnectionListener() throws Exception { final AtomicReference connectionMakerKey2 = new AtomicReference<>(); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory) { + Lock lock = new ReentrantLock(); + @Override - protected synchronized void redeclareElementsIfNecessary() { - connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + protected void redeclareElementsIfNecessary() { + this.lock.lock(); + try { + connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + } + finally { + this.lock.unlock(); + } } - }; container.setQueueNames("foo"); container.setLookupKeyQualifier("xxx"); @@ -261,9 +273,17 @@ public void testWithDMLCAndConnectionListener() throws Exception { final AtomicReference connectionMakerKey2 = new AtomicReference<>(); DirectMessageListenerContainer container = new DirectMessageListenerContainer(connectionFactory) { + Lock lock = new ReentrantLock(); + @Override - protected synchronized void redeclareElementsIfNecessary() { - connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + protected void redeclareElementsIfNecessary() { + this.lock.lock(); + try { + connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + } + finally { + this.lock.unlock(); + } } }; @@ -304,9 +324,17 @@ public void testWithDRTDMLCAndConnectionListenerExistingRFK() throws Exception { final AtomicReference connectionMakerKey2 = new AtomicReference<>(); DirectReplyToMessageListenerContainer container = new DirectReplyToMessageListenerContainer(connectionFactory) { + Lock lock = new ReentrantLock(); + @Override - protected synchronized void redeclareElementsIfNecessary() { - connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + protected void redeclareElementsIfNecessary() { + this.lock.lock(); + try { + connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + } + finally { + this.lock.unlock(); + } } }; @@ -323,4 +351,19 @@ protected synchronized void redeclareElementsIfNecessary() { assertThat(SimpleResourceHolder.unbind(connectionFactory)).isEqualTo("foo"); } + @Test + void afterPropertiesSetShouldNotThrowAnyExceptionAfterAddTargetConnectionFactory() throws Exception { + AbstractRoutingConnectionFactory routingFactory = new AbstractRoutingConnectionFactory() { + @Override + protected Object determineCurrentLookupKey() { + return null; + } + }; + + routingFactory.addTargetConnectionFactory("1", Mockito.mock(ConnectionFactory.class)); + + assertThatNoException() + .isThrownBy(routingFactory::afterPropertiesSet); + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java index d3c50a58de..9f9c84475d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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,16 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.security.SecureRandom; import java.util.Collections; import javax.net.ssl.SSLContext; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.impl.CredentialsProvider; +import com.rabbitmq.client.impl.CredentialsRefreshService; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -39,11 +36,13 @@ import org.springframework.beans.DirectFieldAccessor; import org.springframework.core.io.ClassPathResource; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.impl.CredentialsProvider; -import com.rabbitmq.client.impl.CredentialsRefreshService; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; @@ -76,6 +75,7 @@ public void test() throws Exception { @Test public void testAlgNoProps() throws Exception { RabbitConnectionFactoryBean fb = new RabbitConnectionFactoryBean(); + fb.setMaxInboundMessageBodySize(1000); ConnectionFactory rabbitCf = spy(TestUtils.getPropertyValue(fb, "connectionFactory", ConnectionFactory.class)); new DirectFieldAccessor(fb).setPropertyValue("connectionFactory", rabbitCf); fb.setUseSSL(true); @@ -83,6 +83,7 @@ public void testAlgNoProps() throws Exception { fb.afterPropertiesSet(); fb.getObject(); verify(rabbitCf).useSslProtocol(Mockito.any(SSLContext.class)); + assertThat(rabbitCf).hasFieldOrPropertyWithValue("maxInboundMessageBodySize", 1000); } @Test diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java index 1fe0e11b9c..62c0e41a58 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,12 @@ import java.net.URI; import java.util.List; -import org.springframework.amqp.AmqpException; -import org.springframework.util.StringUtils; - import com.rabbitmq.client.BlockedListener; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpException; +import org.springframework.util.StringUtils; /** * A {@link ConnectionFactory} implementation that returns the same Connections from all {@link #createConnection()} @@ -39,7 +40,7 @@ public class SingleConnectionFactory extends AbstractConnectionFactory { /** Proxy Connection */ - private SharedConnectionProxy connection; + private @Nullable SharedConnectionProxy connection; /** Synchronization monitor for the shared Connection */ private final Object connectionMonitor = new Object(); @@ -64,7 +65,7 @@ public SingleConnectionFactory(int port) { * Create a new SingleConnectionFactory given a host name. * @param hostname the host name to connect to */ - public SingleConnectionFactory(String hostname) { + public SingleConnectionFactory(@Nullable String hostname) { this(hostname, com.rabbitmq.client.ConnectionFactory.DEFAULT_AMQP_PORT); } @@ -73,7 +74,8 @@ public SingleConnectionFactory(String hostname) { * @param hostname the host name to connect to * @param port the port number to connect to */ - public SingleConnectionFactory(String hostname, int port) { + @SuppressWarnings("this-escape") + public SingleConnectionFactory(@Nullable String hostname, int port) { super(new com.rabbitmq.client.ConnectionFactory()); if (!StringUtils.hasText(hostname)) { hostname = getDefaultHostName(); @@ -86,6 +88,7 @@ public SingleConnectionFactory(String hostname, int port) { * Create a new SingleConnectionFactory given a {@link URI}. * @param uri the amqp uri configuring the connection */ + @SuppressWarnings("this-escape") public SingleConnectionFactory(URI uri) { super(new com.rabbitmq.client.ConnectionFactory()); setUri(uri); @@ -152,8 +155,7 @@ public final void destroy() { * @return the new Connection */ protected Connection doCreateConnection() { - Connection connection = createBareConnection(); - return connection; + return createBareConnection(); } @Override @@ -205,16 +207,13 @@ public void close() { } public void destroy() { - if (this.target != null) { - getConnectionListener().onClose(target); - RabbitUtils.closeConnection(this.target); - } - this.target = null; + getConnectionListener().onClose(target); + RabbitUtils.closeConnection(this.target); } @Override public boolean isOpen() { - return target != null && target.isOpen(); + return target.isOpen(); } @Override @@ -224,16 +223,17 @@ public Connection getTargetConnection() { @Override public int getLocalPort() { - Connection target = this.target; - if (target != null) { - return target.getLocalPort(); - } - return 0; + return this.target.getLocalPort(); + } + + @Override + public com.rabbitmq.client.Connection getDelegate() { + return this.target.getDelegate(); } @Override public int hashCode() { - return 31 + ((target == null) ? 0 : target.hashCode()); + return 31 + target.hashCode(); } @Override @@ -248,15 +248,7 @@ public boolean equals(Object obj) { return false; } SharedConnectionProxy other = (SharedConnectionProxy) obj; - if (target == null) { - if (other.target != null) { - return false; - } - } - else if (!target.equals(other.target)) { - return false; - } - return true; + return target.equals(other.target); } @Override diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactoryTests.java index b1fe3753cc..e97ac76794 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,14 @@ package org.springframework.amqp.rabbit.connection; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; +import org.junit.jupiter.api.Test; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -26,15 +34,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import java.util.Collections; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.jupiter.api.Test; - -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; - /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java index fe3ec528ef..1e9105a599 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.Map; import java.util.concurrent.BlockingQueue; @@ -30,6 +24,9 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -50,9 +47,11 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -110,6 +109,36 @@ void testBasic() throws Exception { .isFalse(); } + @Test + void testClose() throws Exception { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + ThreadChannelConnectionFactory tccf = new ThreadChannelConnectionFactory(rabbitConnectionFactory); + Connection conn = tccf.createConnection(); + Channel chann1 = conn.createChannel(false); + Channel targetChannel1 = ((ChannelProxy) chann1).getTargetChannel(); + chann1.close(); + Channel chann2 = conn.createChannel(false); + Channel targetChannel2 = ((ChannelProxy) chann2).getTargetChannel(); + assertThat(chann2).isSameAs(chann1); + assertThat(targetChannel2).isSameAs(targetChannel1); + } + + @Test + void testTxClose() throws Exception { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + ThreadChannelConnectionFactory tccf = new ThreadChannelConnectionFactory(rabbitConnectionFactory); + Connection conn = tccf.createConnection(); + Channel chann1 = conn.createChannel(true); + Channel targetChannel1 = ((ChannelProxy) chann1).getTargetChannel(); + chann1.close(); + Channel chann2 = conn.createChannel(true); + Channel targetChannel2 = ((ChannelProxy) chann2).getTargetChannel(); + assertThat(chann2).isSameAs(chann1); + assertThat(targetChannel2).isSameAs(targetChannel1); + } + @Test void queueDeclared(@Autowired RabbitAdmin admin, @Autowired Config config, @Autowired ThreadChannelConnectionFactory tccf) throws Exception { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java index 12cf466696..08dc224c82 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.willDoNothing; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.io.OutputStream; import java.lang.reflect.Method; import java.time.Duration; @@ -37,6 +27,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.zip.Deflater; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -71,12 +62,19 @@ import org.springframework.amqp.support.postprocessor.ZipPostProcessor; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; -import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.ReflectionUtils; import org.springframework.util.StopWatch; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -236,14 +234,14 @@ public void testSimpleBatchTwoEqualBufferLimit() throws Exception { @Test void testDebatchSMLCSplit() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); testDebatchByContainer(container, false); } @Test void testDebatchSMLC() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); testDebatchByContainer(container, true); } @@ -261,8 +259,8 @@ private void testDebatchByContainer(AbstractMessageListenerContainer container, if (asList) { container.setMessageListener((BatchMessageListener) messages -> { received.addAll(messages); - lastInBatch.add(false); - lastInBatch.add(true); + lastInBatch.add(messages.get(0).getMessageProperties().isLastInBatch()); + lastInBatch.add(messages.get(1).getMessageProperties().isLastInBatch()); batchSize.set(messages.size()); latch.countDown(); }); @@ -303,16 +301,16 @@ private void testDebatchByContainer(AbstractMessageListenerContainer container, @Test public void testDebatchByContainerPerformance() throws Exception { - final List received = new ArrayList(); + final List received = new ArrayList<>(); int count = 100000; final CountDownLatch latch = new CountDownLatch(count); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); container.setQueueNames(ROUTE); - container.setMessageListener((MessageListener) message -> { + container.setMessageListener(message -> { received.add(message); latch.countDown(); }); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); container.setPrefetchCount(1000); container.setBatchSize(1000); container.afterPropertiesSet(); @@ -344,8 +342,9 @@ public void testDebatchByContainerPerformance() throws Exception { public void testDebatchByContainerBadMessageRejected() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); container.setQueueNames(ROUTE); - container.setMessageListener((MessageListener) message -> { }); - container.setReceiveTimeout(100); + container.setMessageListener(message -> { + }); + container.setReceiveTimeout(10); ConditionalRejectingErrorHandler errorHandler = new ConditionalRejectingErrorHandler(); container.setErrorHandler(errorHandler); container.afterPropertiesSet(); @@ -624,23 +623,22 @@ public void testSimpleBatchDeflaterWithEncoding() throws Exception { assertThat(new String(message.getBody())).isEqualTo("\u0000\u0000\u0000\u0003foo\u0000\u0000\u0000\u0003bar"); } - @Nullable - private Message receive(BatchingRabbitTemplate template) throws InterruptedException { + private Message receive(BatchingRabbitTemplate template) { return await().with().pollInterval(Duration.ofMillis(50)) .until(() -> template.receive(ROUTE), msg -> msg != null); } @Test public void testCompressionWithContainer() throws Exception { - final List received = new ArrayList(); + final List received = new ArrayList<>(); final CountDownLatch latch = new CountDownLatch(2); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); container.setQueueNames(ROUTE); - container.setMessageListener((MessageListener) message -> { + container.setMessageListener(message -> { received.add(message); latch.countDown(); }); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); container.setAfterReceivePostProcessors(new DelegatingDecompressingPostProcessor()); container.afterPropertiesSet(); container.start(); @@ -676,11 +674,14 @@ private int getStreamLevel(Object stream) throws Exception { return TestUtils.getPropertyValue(zipStream, "def.level", Integer.class); } - private static class HeaderPostProcessor implements MessagePostProcessor { + private static final class HeaderPostProcessor implements MessagePostProcessor { + @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties().getHeaders().put("someHeader", "someValue"); return message; } + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java index a79977dbfb..8be0ff1d53 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.net.MalformedURLException; import java.net.URISyntaxException; import java.util.Map; @@ -26,7 +23,6 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Binding; @@ -39,9 +35,7 @@ import org.springframework.amqp.core.QueueBuilder.Overflow; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.junit.BrokerRunningSupport; import org.springframework.amqp.rabbit.junit.RabbitAvailable; -import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.beans.factory.annotation.Autowired; @@ -50,9 +44,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.ExchangeInfo; -import com.rabbitmq.http.client.domain.QueueInfo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * @@ -63,9 +56,7 @@ @SpringJUnitConfig @DirtiesContext @RabbitAvailable(management = true) -public class FixedReplyQueueDeadLetterTests { - - private static BrokerRunningSupport brokerRunning; +public class FixedReplyQueueDeadLetterTests extends NeedsManagementTests { @Autowired private RabbitTemplate rabbitTemplate; @@ -73,11 +64,6 @@ public class FixedReplyQueueDeadLetterTests { @Autowired private DeadListener deadListener; - @BeforeAll - static void setUp() { - brokerRunning = RabbitAvailableCondition.getBrokerRunning(); - } - @AfterAll static void tearDown() { brokerRunning.deleteQueues("all.args.1", "all.args.2", "all.args.3", "test.quorum"); @@ -98,10 +84,8 @@ void test() throws Exception { @Test void testQueueArgs1() throws MalformedURLException, URISyntaxException, InterruptedException { - Client client = new Client(brokerRunning.getAdminUri(), brokerRunning.getAdminUser(), - brokerRunning.getAdminPassword()); - QueueInfo queue = await().until(() -> client.getQueue("/", "all.args.1"), que -> que != null); - Map arguments = queue.getArguments(); + Map queue = await().until(() -> queueInfo("all.args.1"), que -> que != null); + Map arguments = arguments(queue); assertThat(arguments.get("x-message-ttl")).isEqualTo(1000); assertThat(arguments.get("x-expires")).isEqualTo(200_000); assertThat(arguments.get("x-max-length")).isEqualTo(42); @@ -117,10 +101,8 @@ void testQueueArgs1() throws MalformedURLException, URISyntaxException, Interrup @Test void testQueueArgs2() throws MalformedURLException, URISyntaxException, InterruptedException { - Client client = new Client(brokerRunning.getAdminUri(), brokerRunning.getAdminUser(), - brokerRunning.getAdminPassword()); - QueueInfo queue = await().until(() -> client.getQueue("/", "all.args.2"), que -> que != null); - Map arguments = queue.getArguments(); + Map queue = await().until(() -> queueInfo("all.args.2"), que -> que != null); + Map arguments = arguments(queue); assertThat(arguments.get("x-message-ttl")).isEqualTo(1000); assertThat(arguments.get("x-expires")).isEqualTo(200_000); assertThat(arguments.get("x-max-length")).isEqualTo(42); @@ -134,11 +116,9 @@ void testQueueArgs2() throws MalformedURLException, URISyntaxException, Interrup } @Test - void testQueueArgs3() throws MalformedURLException, URISyntaxException, InterruptedException { - Client client = new Client(brokerRunning.getAdminUri(), brokerRunning.getAdminUser(), - brokerRunning.getAdminPassword()); - QueueInfo queue = await().until(() -> client.getQueue("/", "all.args.3"), que -> que != null); - Map arguments = queue.getArguments(); + void testQueueArgs3() throws URISyntaxException { + Map queue = await().until(() -> queueInfo("all.args.3"), que -> que != null); + Map arguments = arguments(queue); assertThat(arguments.get("x-message-ttl")).isEqualTo(1000); assertThat(arguments.get("x-expires")).isEqualTo(200_000); assertThat(arguments.get("x-max-length")).isEqualTo(42); @@ -150,19 +130,17 @@ void testQueueArgs3() throws MalformedURLException, URISyntaxException, Interrup assertThat(arguments.get("x-queue-mode")).isEqualTo("lazy"); assertThat(arguments.get(Queue.X_QUEUE_LEADER_LOCATOR)).isEqualTo(LeaderLocator.random.getValue()); - ExchangeInfo exchange = client.getExchange("/", "dlx.test.requestEx"); - assertThat(exchange.getArguments().get("alternate-exchange")).isEqualTo("alternate"); + Map exchange = exchangeInfo("dlx.test.requestEx"); + assertThat(arguments(exchange).get("alternate-exchange")).isEqualTo("alternate"); } /* * Does not require a 3.8 broker - they are just arbitrary arguments. */ @Test - void testQuorumArgs() throws MalformedURLException, URISyntaxException, InterruptedException { - Client client = new Client(brokerRunning.getAdminUri(), brokerRunning.getAdminUser(), - brokerRunning.getAdminPassword()); - QueueInfo queue = await().until(() -> client.getQueue("/", "test.quorum"), que -> que != null); - Map arguments = queue.getArguments(); + void testQuorumArgs() { + Map queue = await().until(() -> queueInfo("test.quorum"), que -> que != null); + Map arguments = arguments(queue); assertThat(arguments.get("x-queue-type")).isEqualTo("quorum"); assertThat(arguments.get("x-delivery-limit")).isEqualTo(10); } @@ -198,6 +176,7 @@ public SimpleMessageListenerContainer replyListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(replyQueue()); + container.setReceiveTimeout(10); container.setMessageListener(fixedReplyQRabbitTemplate()); return container; } @@ -210,6 +189,7 @@ public SimpleMessageListenerContainer serviceListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(requestQueue()); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new PojoListener())); return container; } @@ -222,6 +202,7 @@ public SimpleMessageListenerContainer dlListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(dlq()); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(deadListener())); return container; } @@ -287,7 +268,7 @@ public Queue allArgs1() { return QueueBuilder.nonDurable("all.args.1") .ttl(1000) .expires(200_000) - .maxLength(42) + .maxLength(42L) .maxLengthBytes(10_000) .overflow(Overflow.rejectPublish) .deadLetterExchange("reply.dlx") @@ -304,7 +285,7 @@ public Queue allArgs2() { return QueueBuilder.nonDurable("all.args.2") .ttl(1000) .expires(200_000) - .maxLength(42) + .maxLength(42L) .maxLengthBytes(10_000) .overflow(Overflow.dropHead) .deadLetterExchange("reply.dlx") @@ -320,7 +301,7 @@ public Queue allArgs3() { return QueueBuilder.nonDurable("all.args.3") .ttl(1000) .expires(200_000) - .maxLength(42) + .maxLength(42L) .maxLengthBytes(10_000) .overflow(Overflow.rejectPublish) .deadLetterExchange("reply.dlx") diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java index e48dc3b206..56e69c0159 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2025 the original author or 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.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collections; import java.util.concurrent.TimeUnit; @@ -31,6 +29,8 @@ import org.springframework.amqp.support.AmqpHeaders; import org.springframework.messaging.support.GenericMessage; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.3 @@ -49,7 +49,7 @@ void confirmHeader() throws Exception { CorrelationData data = new CorrelationData(); rmt.send("messaging.confirms", new GenericMessage<>("foo", Collections.singletonMap(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, data))); - assertThat(data.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(data.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); ccf.destroy(); } @@ -65,7 +65,7 @@ void confirmHeaderUnroutable() throws Exception { CorrelationData data = new CorrelationData("foo"); rmt.send("messaging.confirms.unroutable", new GenericMessage<>("foo", Collections.singletonMap(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, data))); - assertThat(data.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(data.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); assertThat(data.getReturned()).isNotNull(); ccf.destroy(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/NeedsManagementTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/NeedsManagementTests.java new file mode 100644 index 0000000000..a854488588 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/NeedsManagementTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 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.amqp.rabbit.core; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; + +import org.junit.jupiter.api.BeforeAll; + +import org.springframework.amqp.rabbit.junit.BrokerRunningSupport; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriUtils; + +/** + * @author Gary Russell + * @since 2.4.8 + * + */ +public abstract class NeedsManagementTests { + + protected static BrokerRunningSupport brokerRunning; + + @BeforeAll + static void setUp() { + brokerRunning = RabbitAvailableCondition.getBrokerRunning(); + } + + protected Map queueInfo(String queueName) throws URISyntaxException { + WebClient client = createClient(brokerRunning.getAdminUser(), brokerRunning.getAdminPassword()); + URI uri = queueUri(queueName); + return client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + } + + protected Map exchangeInfo(String name) throws URISyntaxException { + WebClient client = createClient(brokerRunning.getAdminUser(), brokerRunning.getAdminPassword()); + URI uri = exchangeUri(name); + return client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + } + + @SuppressWarnings("unchecked") + protected Map arguments(Map infoMap) { + return (Map) infoMap.get("arguments"); + } + + private URI queueUri(String queue) throws URISyntaxException { + URI uri = new URI(brokerRunning.getAdminUri()) + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queue); + return uri; + } + + private URI exchangeUri(String queue) throws URISyntaxException { + URI uri = new URI(brokerRunning.getAdminUri()) + .resolve("/api/exchanges/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queue); + return uri; + } + + private WebClient createClient(String adminUser, String adminPassword) { + return WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(adminUser, adminPassword)) + .build(); + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java index 3d82f4c14d..4db98465b8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,30 +16,14 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - import java.io.IOException; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.impl.AMQImpl; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.AmqpAdmin; @@ -50,7 +34,6 @@ import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode; import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionListener; @@ -60,8 +43,20 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.impl.AMQImpl; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -79,8 +74,8 @@ public void testUnconditional() throws Exception { given(cf.createConnection()).willReturn(conn); given(conn.createChannel(false)).willReturn(channel); given(channel.queueDeclare("foo", true, false, false, new HashMap<>())) - .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); - final AtomicReference listener = new AtomicReference(); + .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); + AtomicReference listener = new AtomicReference<>(); willAnswer(invocation -> { listener.set((ConnectionListener) invocation.getArguments()[0]); return null; @@ -100,51 +95,10 @@ public void testUnconditional() throws Exception { listener.get().onCreate(conn); verify(channel).queueDeclare("foo", true, false, false, new HashMap<>()); - verify(channel).exchangeDeclare("bar", "direct", true, false, false, new HashMap()); + verify(channel).exchangeDeclare("bar", "direct", true, false, false, new HashMap<>()); verify(channel).queueBind("foo", "bar", "foo", new HashMap<>()); } - @Test - public void testNoDeclareWithCachedConnections() throws Exception { - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - - final List mockChannels = new ArrayList(); - - AtomicInteger connectionNumber = new AtomicInteger(); - willAnswer(invocation -> { - com.rabbitmq.client.Connection connection = mock(com.rabbitmq.client.Connection.class); - AtomicInteger channelNumber = new AtomicInteger(); - willAnswer(invocation1 -> { - Channel channel = mock(Channel.class); - given(channel.isOpen()).willReturn(true); - int channelNum = channelNumber.incrementAndGet(); - given(channel.toString()).willReturn("mockChannel" + channelNum); - mockChannels.add(channel); - return channel; - }).given(connection).createChannel(); - int connectionNum = connectionNumber.incrementAndGet(); - given(connection.toString()).willReturn("mockConnection" + connectionNum); - given(connection.isOpen()).willReturn(true); - return connection; - }).given(mockConnectionFactory).newConnection((ExecutorService) null); - - CachingConnectionFactory ccf = new CachingConnectionFactory(mockConnectionFactory); - ccf.setCacheMode(CacheMode.CONNECTION); - ccf.afterPropertiesSet(); - - RabbitAdmin admin = new RabbitAdmin(ccf); - GenericApplicationContext context = new GenericApplicationContext(); - Queue queue = new Queue("foo"); - context.getBeanFactory().registerSingleton("foo", queue); - context.refresh(); - admin.setApplicationContext(context); - admin.afterPropertiesSet(); - ccf.createConnection().close(); - ccf.destroy(); - - assertThat(mockChannels.size()).as("Admin should not have created a channel").isEqualTo(0); - } - @Test public void testUnconditionalWithExplicitFactory() throws Exception { ConnectionFactory cf = mock(ConnectionFactory.class); @@ -153,8 +107,8 @@ public void testUnconditionalWithExplicitFactory() throws Exception { given(cf.createConnection()).willReturn(conn); given(conn.createChannel(false)).willReturn(channel); given(channel.queueDeclare("foo", true, false, false, new HashMap<>())) - .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); - final AtomicReference listener = new AtomicReference(); + .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); + AtomicReference listener = new AtomicReference<>(); willAnswer(invocation -> { listener.set(invocation.getArgument(0)); return null; @@ -177,7 +131,7 @@ public void testUnconditionalWithExplicitFactory() throws Exception { listener.get().onCreate(conn); verify(channel).queueDeclare("foo", true, false, false, new HashMap<>()); - verify(channel).exchangeDeclare("bar", "direct", true, false, false, new HashMap()); + verify(channel).exchangeDeclare("bar", "direct", true, false, false, new HashMap<>()); verify(channel).queueBind("foo", "bar", "foo", new HashMap<>()); } @@ -189,8 +143,9 @@ public void testSkipBecauseDifferentFactory() throws Exception { Channel channel = mock(Channel.class); given(cf.createConnection()).willReturn(conn); given(conn.createChannel(false)).willReturn(channel); - given(channel.queueDeclare("foo", true, false, false, null)).willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); - final AtomicReference listener = new AtomicReference(); + given(channel.queueDeclare("foo", true, false, false, null)) + .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); + AtomicReference listener = new AtomicReference<>(); willAnswer(invocation -> { listener.set(invocation.getArgument(0)); return null; @@ -215,20 +170,21 @@ public void testSkipBecauseDifferentFactory() throws Exception { verify(channel, never()).queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); verify(channel, never()) - .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); + .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); verify(channel, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), any(Map.class)); } @SuppressWarnings("unchecked") @Test - public void testSkipBecauseShouldntDeclare() throws Exception { + public void testSkipBecauseShouldNotDeclare() throws Exception { ConnectionFactory cf = mock(ConnectionFactory.class); Connection conn = mock(Connection.class); Channel channel = mock(Channel.class); given(cf.createConnection()).willReturn(conn); given(conn.createChannel(false)).willReturn(channel); - given(channel.queueDeclare("foo", true, false, false, null)).willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); - final AtomicReference listener = new AtomicReference(); + given(channel.queueDeclare("foo", true, false, false, null)) + .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); + AtomicReference listener = new AtomicReference<>(); willAnswer(invocation -> { listener.set(invocation.getArgument(0)); return null; @@ -252,7 +208,7 @@ public void testSkipBecauseShouldntDeclare() throws Exception { verify(channel, never()).queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); verify(channel, never()) - .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); + .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); verify(channel, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), any(Map.class)); } @@ -263,9 +219,8 @@ public void testJavaConfig() throws Exception { verify(Config.channel1).queueDeclare("foo", true, false, false, new HashMap<>()); verify(Config.channel1, never()).queueDeclare("baz", true, false, false, new HashMap<>()); verify(Config.channel1).queueDeclare("qux", true, false, false, new HashMap<>()); - verify(Config.channel1).exchangeDeclare("bar", "direct", true, false, true, new HashMap()); + verify(Config.channel1).exchangeDeclare("bar", "direct", true, false, true, new HashMap<>()); verify(Config.channel1).queueBind("foo", "bar", "foo", new HashMap<>()); - Config.listener2.onCreate(Config.conn2); verify(Config.channel2, never()) .queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), isNull()); @@ -273,9 +228,8 @@ public void testJavaConfig() throws Exception { verify(Config.channel2).queueDeclare("qux", true, false, false, new HashMap<>()); verify(Config.channel2, never()) .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), - anyBoolean(), anyMap()); + anyBoolean(), anyMap()); verify(Config.channel2, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), anyMap()); - Config.listener3.onCreate(Config.conn3); verify(Config.channel3, never()) .queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), isNull()); @@ -286,7 +240,7 @@ public void testJavaConfig() throws Exception { verify(Config.channel3, never()).queueDeclare("qux", true, false, false, new HashMap<>()); verify(Config.channel3, never()) .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), - anyBoolean(), anyMap()); + anyBoolean(), anyMap()); verify(Config.channel3, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), anyMap()); context.close(); @@ -316,13 +270,9 @@ public void testAddRemove() { assertThat(queue.getDeclaringAdmins()).hasSize(2); queue.setAdminsThatShouldDeclare((Object[]) null); assertThat(queue.getDeclaringAdmins()).hasSize(0); - try { - queue.setAdminsThatShouldDeclare(null, admin1); - fail("Expected Exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage()).contains("'admins' cannot contain null elements"); - } + assertThatIllegalArgumentException() + .isThrownBy(() -> queue.setAdminsThatShouldDeclare(null, admin1)) + .withMessageContaining("'admins' cannot contain null elements"); } @Test @@ -348,17 +298,17 @@ public void testNoOpWhenNothingToDeclare() throws Exception { @Configuration public static class Config { - private static Connection conn1 = mock(Connection.class); + private static final Connection conn1 = mock(); - private static Connection conn2 = mock(Connection.class); + private static final Connection conn2 = mock(); - private static Connection conn3 = mock(Connection.class); + private static final Connection conn3 = mock(); - private static Channel channel1 = mock(Channel.class); + private static final Channel channel1 = mock(); - private static Channel channel2 = mock(Channel.class); + private static final Channel channel2 = mock(); - private static Channel channel3 = mock(Channel.class); + private static final Channel channel3 = mock(); private static ConnectionListener listener1; @@ -371,9 +321,9 @@ public ConnectionFactory cf1() throws IOException { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(conn1); given(conn1.createChannel(false)).willReturn(channel1); - willAnswer(inv -> { - return new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0); - }).given(channel1).queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); + willAnswer(inv -> new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0)) + .given(channel1) + .queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); willAnswer(invocation -> { listener1 = invocation.getArgument(0); return null; @@ -386,9 +336,9 @@ public ConnectionFactory cf2() throws IOException { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(conn2); given(conn2.createChannel(false)).willReturn(channel2); - willAnswer(inv -> { - return new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0); - }).given(channel2).queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); + willAnswer(inv -> new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0)) + .given(channel2) + .queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); willAnswer(invocation -> { listener2 = invocation.getArgument(0); return null; @@ -401,9 +351,9 @@ public ConnectionFactory cf3() throws IOException { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(conn3); given(conn3.createChannel(false)).willReturn(channel3); - willAnswer(inv -> { - return new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0); - }).given(channel3).queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); + willAnswer(inv -> new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0)) + .given(channel3) + .queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); willAnswer(invocation -> { listener3 = invocation.getArgument(0); return null; @@ -413,14 +363,12 @@ public ConnectionFactory cf3() throws IOException { @Bean public RabbitAdmin admin1() throws IOException { - RabbitAdmin rabbitAdmin = new RabbitAdmin(cf1()); - return rabbitAdmin; + return new RabbitAdmin(cf1()); } @Bean public RabbitAdmin admin2() throws IOException { - RabbitAdmin rabbitAdmin = new RabbitAdmin(cf2()); - return rabbitAdmin; + return new RabbitAdmin(cf2()); } @Bean diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java index 1c504a7c78..47b072f210 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,16 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.awaitility.Awaitility.await; - import java.io.IOException; import java.time.Duration; +import java.util.Map; +import java.util.Objects; import java.util.UUID; +import com.rabbitmq.client.AMQP.Queue.DeclareOk; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -47,12 +49,10 @@ import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.context.support.GenericApplicationContext; -import com.rabbitmq.client.AMQP.Queue.DeclareOk; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.ExchangeInfo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; /** * @author Dave Syer @@ -60,9 +60,10 @@ * @author Gary Russell * @author Gunnar Hillert * @author Artem Bilan + * @author Raylax Grey */ @RabbitAvailable(management = true) -public class RabbitAdminIntegrationTests { +public class RabbitAdminIntegrationTests extends NeedsManagementTests { private final CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); @@ -131,7 +132,7 @@ public void testDoubleDeclarationOfExclusiveQueue() { @Test public void testDoubleDeclarationOfAutodeleteQueue() { - // No error expected here: the queue is autodeleted when the last consumer is cancelled, but this one never has + // No error expected here: the queue is auto-deleted when the last consumer is cancelled, but this one never has // any consumers. CachingConnectionFactory connectionFactory1 = new CachingConnectionFactory(); connectionFactory1.setHost("localhost"); @@ -237,9 +238,7 @@ public void testSpringWithDefaultExchange() { context.getBeanFactory().registerSingleton("foo", exchange); rabbitAdmin.afterPropertiesSet(); - rabbitAdmin.initialize(); - - // Pass by virtue of RabbitMQ not firing a 403 reply code + assertThatNoException().isThrownBy(rabbitAdmin::initialize); } @Test @@ -256,9 +255,9 @@ public void testDeleteExchangeWithInternalOption() throws Exception { exchange.setInternal(true); rabbitAdmin.declareExchange(exchange); - ExchangeInfo exchange2 = getExchange(exchangeName); - assertThat(exchange2.getType()).isEqualTo(ExchangeTypes.DIRECT); - assertThat(exchange2.isInternal()).isTrue(); + Map exchange2 = getExchange(exchangeName); + assertThat(exchange2.get("type")).isEqualTo(ExchangeTypes.DIRECT); + assertThat(exchange2.get("internal")).isEqualTo(Boolean.TRUE); boolean result = rabbitAdmin.deleteExchange(exchangeName); @@ -397,34 +396,33 @@ public void testDeclareDelayedExchange() throws Exception { RabbitTemplate template = new RabbitTemplate(this.connectionFactory); template.setReceiveTimeout(10000); template.convertAndSend(exchangeName, queue.getName(), "foo", message -> { - message.getMessageProperties().setDelay(1000); + message.getMessageProperties().setDelayLong(1000L); return message; }); MessageProperties properties = new MessageProperties(); - properties.setDelay(500); + properties.setDelayLong(500L); template.send(exchangeName, queue.getName(), MessageBuilder.withBody("foo".getBytes()).andProperties(properties).build()); long t1 = System.currentTimeMillis(); Message received = template.receive(queue.getName()); assertThat(received).isNotNull(); - assertThat(received.getMessageProperties().getReceivedDelay()).isEqualTo(Integer.valueOf(500)); + assertThat(received.getMessageProperties().getDelayLong()).isEqualTo(500L); received = template.receive(queue.getName()); assertThat(received).isNotNull(); - assertThat(received.getMessageProperties().getReceivedDelay()).isEqualTo(Integer.valueOf(1000)); + assertThat(received.getMessageProperties().getDelayLong()).isEqualTo(1000L); assertThat(System.currentTimeMillis() - t1).isGreaterThan(950L); - ExchangeInfo exchange2 = getExchange(exchangeName); - assertThat(exchange2.getArguments().get("x-delayed-type")).isEqualTo(ExchangeTypes.DIRECT); - assertThat(exchange2.getType()).isEqualTo("x-delayed-message"); + Map exchange2 = getExchange(exchangeName); + assertThat(arguments(exchange2).get("x-delayed-type")).isEqualTo(ExchangeTypes.DIRECT); + assertThat(exchange2.get("type")).isEqualTo("x-delayed-message"); this.rabbitAdmin.deleteQueue(queue.getName()); this.rabbitAdmin.deleteExchange(exchangeName); } - private ExchangeInfo getExchange(String exchangeName) throws Exception { - Client rabbitRestClient = new Client("http://localhost:15672/api/", "guest", "guest"); + private Map getExchange(String exchangeName) { return await().pollDelay(Duration.ZERO) - .until(() -> rabbitRestClient.getExchange("/", exchangeName), exch -> exch != null); + .until(() -> exchangeInfo(exchangeName), Objects::nonNull); } /** @@ -438,18 +436,14 @@ private boolean queueExists(final Queue queue) throws Exception { ConnectionFactory cf = new ConnectionFactory(); cf.setHost("localhost"); cf.setPort(BrokerTestUtils.getPort()); - Connection connection = cf.newConnection(); - Channel channel = connection.createChannel(); - try { + try (Connection connection = cf.newConnection()) { + Channel channel = connection.createChannel(); DeclareOk result = channel.queueDeclarePassive(queue.getName()); return result != null; } catch (IOException e) { return e.getCause().getMessage().contains("RESOURCE_LOCKED"); } - finally { - connection.close(); - } } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java index d298c3f73c..2cacb164ff 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,35 +16,20 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willDoNothing; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeoutException; +import com.rabbitmq.client.AMQP.Queue.DeclareOk; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -81,11 +66,24 @@ import org.springframework.retry.backoff.NoBackOffPolicy; import org.springframework.retry.support.RetryTemplate; -import com.rabbitmq.client.AMQP.Queue.DeclareOk; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * @author Mark Pollack @@ -99,7 +97,7 @@ * */ @RabbitAvailable(management = true) -public class RabbitAdminTests { +public class RabbitAdminTests extends NeedsManagementTests { @Test public void testSettingOfNullConnectionFactory() { @@ -165,7 +163,7 @@ public void testProperties() throws Exception { } } - private int messageCount(RabbitAdmin rabbitAdmin, String queueName) { + private long messageCount(RabbitAdmin rabbitAdmin, String queueName) { QueueInformation info = rabbitAdmin.getQueueInfo(queueName); assertThat(info).isNotNull(); return info.getMessageCount(); @@ -373,17 +371,127 @@ public void testLeaderLocator() throws Exception { RabbitAdmin admin = new RabbitAdmin(cf); AnonymousQueue queue = new AnonymousQueue(); admin.declareQueue(queue); - Client client = new Client("http://guest:guest@localhost:15672/api"); AnonymousQueue queue1 = queue; - QueueInfo info = await().until(() -> client.getQueue("/", queue1.getName()), inf -> inf != null); - assertThat(info.getArguments().get(Queue.X_QUEUE_LEADER_LOCATOR)).isEqualTo("client-local"); + Map info = await().until(() -> queueInfo(queue1.getName()), inf -> inf != null); + assertThat(arguments(info).get(Queue.X_QUEUE_LEADER_LOCATOR)).isEqualTo("client-local"); queue = new AnonymousQueue(); queue.setLeaderLocator(null); admin.declareQueue(queue); AnonymousQueue queue2 = queue; - info = await().until(() -> client.getQueue("/", queue2.getName()), inf -> inf != null); - assertThat(info.getArguments().get(Queue.X_QUEUE_LEADER_LOCATOR)).isNull(); + info = await().until(() -> queueInfo(queue2.getName()), inf -> inf != null); + assertThat(arguments(info).get(Queue.X_QUEUE_LEADER_LOCATOR)).isNull(); + cf.destroy(); + } + + @Test + void manualDeclarations() { + CachingConnectionFactory cf = new CachingConnectionFactory( + RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + RabbitAdmin admin = new RabbitAdmin(cf); + GenericApplicationContext applicationContext = new GenericApplicationContext(); + admin.setApplicationContext(applicationContext); + admin.setRedeclareManualDeclarations(true); + applicationContext.registerBean("admin", RabbitAdmin.class, () -> admin); + applicationContext.registerBean("beanQueue", Queue.class, + () -> new Queue("thisOneShouldntBeInTheManualDecs", false, true, true)); + applicationContext.registerBean("beanEx", DirectExchange.class, + () -> new DirectExchange("thisOneShouldntBeInTheManualDecs", false, true)); + applicationContext.registerBean("beanBinding", Binding.class, + () -> new Binding("thisOneShouldntBeInTheManualDecs", DestinationType.QUEUE, + "thisOneShouldntBeInTheManualDecs", "test", null)); + applicationContext.refresh(); + Set declarables = TestUtils.getPropertyValue(admin, "manualDeclarables", Set.class); + assertThat(declarables).hasSize(0); + // check the auto-configured Declarables + RabbitTemplate template = new RabbitTemplate(cf); + template.convertAndSend("thisOneShouldntBeInTheManualDecs", "test", "foo"); + Object received = template.receiveAndConvert("thisOneShouldntBeInTheManualDecs", 5000); + assertThat(received).isEqualTo("foo"); + // manual declarations + admin.declareExchange(new DirectExchange("test1", false, true)); + admin.declareQueue(new Queue("test1", false, true, true)); + admin.declareQueue(new Queue("test2", false, true, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "test1", "test", null)); + admin.deleteQueue("test2"); + template.execute(chan -> { + chan.queueDelete("test1"); + chan.exchangeDelete("test1"); + return null; + }); + cf.resetConnection(); + admin.initialize(); + assertThat(admin.getQueueProperties("test1")).isNotNull(); + assertThat(admin.getQueueProperties("test2")).isNull(); + assertThat(declarables).hasSize(3); + // verify the exchange and binding were recovered too + template.convertAndSend("test1", "test", "foo"); + received = template.receiveAndConvert("test1", 5000); + assertThat(received).isEqualTo("foo"); + admin.resetAllManualDeclarations(); + assertThat(declarables).hasSize(0); + cf.resetConnection(); + admin.initialize(); + assertThat(admin.getQueueProperties("test1")).isNull(); + admin.declareQueue(new Queue("test1", false, true, true)); + admin.declareExchange(new DirectExchange("ex1", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex1", "test", null)); + admin.declareExchange(new DirectExchange("ex2", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex2", "test", null)); + admin.declareBinding(new Binding("ex1", DestinationType.EXCHANGE, "ex2", "ex1", null)); + assertThat(declarables).hasSize(6); + admin.deleteExchange("ex2"); + assertThat(declarables).hasSize(3); + admin.deleteQueue("test1"); + assertThat(declarables).hasSize(1); + admin.deleteExchange("ex1"); + assertThat(declarables).hasSize(0); + cf.destroy(); + } + + @Test + void manualDeclarationsWithoutApplicationContext() { + CachingConnectionFactory cf = new CachingConnectionFactory( + RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + RabbitAdmin admin = new RabbitAdmin(cf); + admin.setRedeclareManualDeclarations(true); + Set declarables = TestUtils.getPropertyValue(admin, "manualDeclarables", Set.class); + assertThat(declarables).hasSize(0); + RabbitTemplate template = new RabbitTemplate(cf); + // manual declarations + admin.declareQueue(new Queue("test1", false, true, true)); + admin.declareQueue(new Queue("test2", false, true, true)); + admin.declareExchange(new DirectExchange("ex1", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex1", "test", null)); + admin.deleteQueue("test2"); + template.execute(chan -> chan.queueDelete("test1")); + cf.resetConnection(); + admin.initialize(); + assertThat(admin.getQueueProperties("test1")).isNotNull(); + assertThat(admin.getQueueProperties("test2")).isNull(); + assertThat(declarables).hasSize(3); + // verify the exchange and binding were recovered too + template.convertAndSend("ex1", "test", "foo"); + Object received = template.receiveAndConvert("test1", 5000); + assertThat(received).isEqualTo("foo"); + admin.resetAllManualDeclarations(); + assertThat(declarables).hasSize(0); + cf.resetConnection(); + admin.initialize(); + assertThat(admin.getQueueProperties("test1")).isNull(); + admin.declareQueue(new Queue("test1", false, true, true)); + admin.declareExchange(new DirectExchange("ex1", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex1", "test", null)); + admin.declareExchange(new DirectExchange("ex2", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex2", "test", null)); + admin.declareBinding(new Binding("ex1", DestinationType.EXCHANGE, "ex2", "ex1", null)); + assertThat(declarables).hasSize(6); + admin.deleteExchange("ex2"); + assertThat(declarables).hasSize(3); + admin.deleteQueue("test1"); + assertThat(declarables).hasSize(1); + admin.deleteExchange("ex1"); + assertThat(declarables).hasSize(0); cf.destroy(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitBindingIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitBindingIntegrationTests.java index 317d06f220..867d6b455f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitBindingIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitBindingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.time.Duration; import org.junit.jupiter.api.AfterEach; @@ -40,6 +37,9 @@ import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + /** * @author Dave Syer * @author Gunnar Hillert diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupportTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupportTests.java index 4d29a8340f..e740e054e0 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupportTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 the original author or authors. + * Copyright 2010-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Mark Pollack * @author Chris Beams diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java index f1db80c69d..167918ff8c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -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 java.io.Writer; import java.util.Collections; import java.util.HashMap; @@ -55,10 +44,22 @@ import org.springframework.messaging.support.GenericMessage; import org.springframework.messaging.support.MessageBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + /** * @author Stephane Nicoll * @author Gary Russell + * @author Artem Bilan */ public class RabbitMessagingTemplateTests { @@ -75,6 +76,7 @@ public class RabbitMessagingTemplateTests { @BeforeEach public void setup() { this.openMocks = MockitoAnnotations.openMocks(this); + given(this.rabbitTemplate.getMessageConverter()).willReturn(new SimpleMessageConverter()); messagingTemplate = new RabbitMessagingTemplate(rabbitTemplate); } @@ -211,7 +213,7 @@ protected org.springframework.amqp.core.Message createMessage(Object object, assertThatThrownBy(() -> messagingTemplate.convertAndSend("myQueue", "msg to convert")) .isExactlyInstanceOf(org.springframework.messaging.converter.MessageConversionException.class) - .hasMessageContaining("Test exception"); + .hasStackTraceContaining("Test exception"); } @Test @@ -256,6 +258,20 @@ public void receiveDefaultDestination() { assertTextMessage(message); } + @Test + public void receiveDefaultDestinationOverride() { + messagingTemplate.setDefaultDestination("defaultDest"); + messagingTemplate.setUseTemplateDefaultReceiveQueue(true); + + org.springframework.amqp.core.Message amqpMsg = createAmqpTextMessage(); + given(rabbitTemplate.getDefaultReceiveQueue()).willReturn("default"); + given(rabbitTemplate.receive("default")).willReturn(amqpMsg); + + Message message = messagingTemplate.receive(); + verify(rabbitTemplate).receive("default"); + assertTextMessage(message); + } + @Test public void receiveNoDefaultSet() { assertThatIllegalStateException() diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java deleted file mode 100644 index 4a574ad87f..0000000000 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.rabbit.core; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - -import java.net.MalformedURLException; -import java.net.URISyntaxException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.DirectExchange; -import org.springframework.amqp.core.Exchange; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.core.QueueBuilder; -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.junit.RabbitAvailable; - -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.BindingInfo; -import com.rabbitmq.http.client.domain.ExchangeInfo; -import com.rabbitmq.http.client.domain.QueueInfo; - -/** - * @author Gary Russell - * @author Artem Bilan - * - * @since 1.5 - * - */ -@RabbitAvailable(management = true) -public class RabbitRestApiTests { - - private final CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost"); - - private final Client rabbitRestClient; - - public RabbitRestApiTests() throws MalformedURLException, URISyntaxException { - this.rabbitRestClient = new Client("http://localhost:15672/api/", "guest", "guest"); - } - - @AfterEach - public void tearDown() { - connectionFactory.destroy(); - } - - @Test - public void testExchanges() { - List list = this.rabbitRestClient.getExchanges(); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testExchangesVhost() { - List list = this.rabbitRestClient.getExchanges("/"); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testBindings() { - List list = this.rabbitRestClient.getBindings(); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testBindingsVhost() { - List list = this.rabbitRestClient.getBindings("/"); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testQueues() { - List list = this.rabbitRestClient.getQueues(); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testQueuesVhost() { - List list = this.rabbitRestClient.getQueues("/"); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testBindingsDetail() { - RabbitAdmin admin = new RabbitAdmin(connectionFactory); - Map args = Collections.singletonMap("alternate-exchange", ""); - Exchange exchange1 = new DirectExchange(UUID.randomUUID().toString(), false, true, args); - admin.declareExchange(exchange1); - Exchange exchange2 = new DirectExchange(UUID.randomUUID().toString(), false, true, args); - admin.declareExchange(exchange2); - Queue queue = admin.declareQueue(); - Binding binding1 = BindingBuilder - .bind(queue) - .to(exchange1) - .with("foo") - .and(args); - admin.declareBinding(binding1); - Binding binding2 = BindingBuilder - .bind(exchange2) - .to((DirectExchange) exchange1) - .with("bar"); - admin.declareBinding(binding2); - - List bindings = this.rabbitRestClient.getBindingsBySource("/", exchange1.getName()); - assertThat(bindings).hasSize(2); - assertThat(bindings.get(0).getSource()).isEqualTo(exchange1.getName()); - assertThat("foo").isIn(bindings.get(0).getRoutingKey(), bindings.get(1).getRoutingKey()); - BindingInfo qout = null; - BindingInfo eout = null; - if (bindings.get(0).getRoutingKey().equals("foo")) { - qout = bindings.get(0); - eout = bindings.get(1); - } - else { - eout = bindings.get(0); - qout = bindings.get(1); - } - assertThat(qout.getDestinationType()).isEqualTo("queue"); - assertThat(qout.getDestination()).isEqualTo(queue.getName()); - assertThat(qout.getArguments()).isNotNull(); - assertThat(qout.getArguments().get("alternate-exchange")).isEqualTo(""); - - assertThat(eout.getDestinationType()).isEqualTo("exchange"); - assertThat(eout.getDestination()).isEqualTo(exchange2.getName()); - - admin.deleteExchange(exchange1.getName()); - admin.deleteExchange(exchange2.getName()); - } - - @Test - public void testSpecificExchange() { - RabbitAdmin admin = new RabbitAdmin(connectionFactory); - Map args = Collections.singletonMap("alternate-exchange", ""); - Exchange exchange = new DirectExchange(UUID.randomUUID().toString(), true, true, args); - admin.declareExchange(exchange); - ExchangeInfo exchangeOut = this.rabbitRestClient.getExchange("/", exchange.getName()); - assertThat(exchangeOut.isDurable()).isTrue(); - assertThat(exchangeOut.isAutoDelete()).isTrue(); - assertThat(exchangeOut.getName()).isEqualTo(exchange.getName()); - assertThat(exchangeOut.getArguments()).isEqualTo(args); - admin.deleteExchange(exchange.getName()); - } - - @Test - public void testSpecificQueue() throws Exception { - RabbitAdmin admin = new RabbitAdmin(connectionFactory); - Map args = Collections.singletonMap("foo", "bar"); - Queue queue1 = QueueBuilder.nonDurable(UUID.randomUUID().toString()) - .autoDelete() - .withArguments(args) - .build(); - admin.declareQueue(queue1); - Queue queue2 = QueueBuilder.durable(UUID.randomUUID().toString()) - .withArguments(args) - .build(); - admin.declareQueue(queue2); - Channel channel = this.connectionFactory.createConnection().createChannel(false); - String consumer = channel.basicConsume(queue1.getName(), false, "", false, true, null, new DefaultConsumer(channel)); - QueueInfo qi = await().until(() -> this.rabbitRestClient.getQueue("/", queue1.getName()), - info -> info.getExclusiveConsumerTag() != null && !"".equals(info.getExclusiveConsumerTag())); - QueueInfo queueOut = this.rabbitRestClient.getQueue("/", queue1.getName()); - assertThat(queueOut.isDurable()).isFalse(); - assertThat(queueOut.isExclusive()).isFalse(); - assertThat(queueOut.isAutoDelete()).isTrue(); - assertThat(queueOut.getName()).isEqualTo(queue1.getName()); - assertThat(queueOut.getArguments()).isEqualTo(args); - assertThat(qi.getExclusiveConsumerTag()).isEqualTo(consumer); - channel.basicCancel(consumer); - channel.close(); - - queueOut = this.rabbitRestClient.getQueue("/", queue2.getName()); - assertThat(queueOut.isDurable()).isTrue(); - assertThat(queueOut.isExclusive()).isFalse(); - assertThat(queueOut.isAutoDelete()).isFalse(); - assertThat(queueOut.getName()).isEqualTo(queue2.getName()); - assertThat(queueOut.getArguments()).isEqualTo(args); - - admin.deleteQueue(queue1.getName()); - admin.deleteQueue(queue2.getName()); - } - - @Test - public void testDeleteExchange() { - String exchangeName = "testExchange"; - Exchange testExchange = new DirectExchange(exchangeName); - ExchangeInfo info = new ExchangeInfo(); - info.setArguments(testExchange.getArguments()); - info.setAutoDelete(testExchange.isAutoDelete()); - info.setDurable(testExchange.isDurable()); - info.setType(testExchange.getType()); - this.rabbitRestClient.declareExchange("/", testExchange.getName(), info); - ExchangeInfo exchangeToAssert = this.rabbitRestClient.getExchange("/", exchangeName); - assertThat(exchangeToAssert.getName()).isEqualTo(testExchange.getName()); - assertThat(exchangeToAssert.getType()).isEqualTo(testExchange.getType()); - this.rabbitRestClient.deleteExchange("/", testExchange.getName()); - assertThat(this.rabbitRestClient.getExchange("/", exchangeName)).isNull(); - } - -} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateDirectReplyToContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateDirectReplyToContainerIntegrationTests.java index 1b6102908d..2dd13583d8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateDirectReplyToContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateDirectReplyToContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; - import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -27,6 +23,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpRejectAndDontRequeueException; @@ -38,7 +35,9 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.util.ErrorHandler; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateHeaderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateHeaderTests.java index 8754a999d7..d5af8747b4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateHeaderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateHeaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,16 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -37,10 +34,12 @@ import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java index 6dd054befc..5baa4dfd70 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,8 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.io.IOException; +import java.io.PrintStream; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collection; @@ -52,6 +37,16 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.AlreadyClosedException; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.GetResponse; +import com.rabbitmq.client.Method; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.AMQImpl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -73,7 +68,6 @@ import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.ReceiveAndReplyCallback; import org.springframework.amqp.core.ReceiveAndReplyMessageCallback; -import org.springframework.amqp.core.ReplyToAddressCallback; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; import org.springframework.amqp.rabbit.connection.ChannelListener; @@ -123,16 +117,21 @@ import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.ReflectionUtils; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.AlreadyClosedException; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.GetResponse; -import com.rabbitmq.client.Method; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.client.impl.AMQImpl; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Dave Syer @@ -143,12 +142,12 @@ * @author Artem Bilan */ @SpringJUnitConfig -@RabbitAvailable({ RabbitTemplateIntegrationTests.ROUTE, RabbitTemplateIntegrationTests.REPLY_QUEUE_NAME, - RabbitTemplateIntegrationTests.NO_CORRELATION }) -@LogLevels(classes = { RabbitTemplate.class, DirectMessageListenerContainer.class, - DirectReplyToMessageListenerContainer.class, - RabbitAdmin.class, RabbitTemplateIntegrationTests.class, BrokerRunning.class, - ClosingRecoveryListener.class }, +@RabbitAvailable({RabbitTemplateIntegrationTests.ROUTE, RabbitTemplateIntegrationTests.REPLY_QUEUE_NAME, + RabbitTemplateIntegrationTests.NO_CORRELATION}) +@LogLevels(classes = {RabbitTemplate.class, DirectMessageListenerContainer.class, + DirectReplyToMessageListenerContainer.class, + RabbitAdmin.class, RabbitTemplateIntegrationTests.class, BrokerRunning.class, + ClosingRecoveryListener.class}, level = "DEBUG") @DirtiesContext public class RabbitTemplateIntegrationTests { @@ -335,8 +334,10 @@ class MockChannel extends PublisherCallbackChannelImpl { } @Override - public String basicConsume(String queue, Consumer callback) throws IOException { - return super.basicConsume(queue, new MockConsumer(callback)); + public String basicConsume(String queue, boolean autoAck, Map args, Consumer callback) + throws IOException { + + return super.basicConsume(queue, autoAck, args, new MockConsumer(callback)); } } @@ -392,7 +393,7 @@ public void testReceiveTimeoutRequeue() { // empty - race for consumeOk } assertThat(TestUtils.getPropertyValue(this.connectionFactory, "cachedChannelsNonTransactional", List.class) - ).hasSize(0); + ).hasSize(0); } @Test @@ -448,19 +449,19 @@ public void testSendToNonExistentAndThenReceive() throws Exception { @Test public void testSendAndReceiveWithPostProcessor() throws Exception { - final String[] strings = new String[] { "1", "2" }; + final String[] strings = new String[] {"1", "2"}; template.convertAndSend(ROUTE, (Object) "message", message -> { message.getMessageProperties().setContentType("text/other"); // message.getMessageProperties().setUserId("foo"); MessageProperties props = message.getMessageProperties(); props.getHeaders().put("strings", strings); - props.getHeaders().put("objects", new Object[] { new Foo(), new Foo() }); + props.getHeaders().put("objects", new Object[] {new Foo(), new Foo()}); props.getHeaders().put("bytes", "abc".getBytes()); return message; }); template.setAfterReceivePostProcessors(message -> { assertThat(message.getMessageProperties().getHeaders().get("strings")).isEqualTo(Arrays.asList(strings)); - assertThat(message.getMessageProperties().getHeaders().get("objects")).isEqualTo(Arrays.asList(new String[]{"FooAsAString", "FooAsAString"})); + assertThat(message.getMessageProperties().getHeaders().get("objects")).isEqualTo(Arrays.asList(new String[] {"FooAsAString", "FooAsAString"})); assertThat((byte[]) message.getMessageProperties().getHeaders().get("bytes")).isEqualTo("abc".getBytes()); return message; }); @@ -720,6 +721,7 @@ public void testAtomicSendAndReceiveUserCorrelation() throws Exception { template.setReplyTimeout(10000); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cachingConnectionFactory); container.setQueues(replyQueue); + container.setReceiveTimeout(10); container.setMessageListener(template); container.afterPropertiesSet(); container.start(); @@ -727,10 +729,10 @@ public void testAtomicSendAndReceiveUserCorrelation() throws Exception { messageProperties.setCorrelationId("myCorrelationId"); Message message = new Message("test-message".getBytes(), messageProperties); Message reply = template.sendAndReceive(message); - assertThat(new String(received.get(1000, TimeUnit.MILLISECONDS).getBody())).isEqualTo(new String(message.getBody())); + assertThat(received.get(1000, TimeUnit.MILLISECONDS).getBody()).isEqualTo(message.getBody()); assertThat(reply).as("Reply is expected").isNotNull(); assertThat(remoteCorrelationId.get()).isEqualTo("myCorrelationId"); - assertThat(new String(reply.getBody())).isEqualTo(new String(message.getBody())); + assertThat(reply.getBody()).isEqualTo(message.getBody()); // Message was consumed so nothing left on queue reply = template.receive(); assertThat(reply).isEqualTo(null); @@ -1181,9 +1183,9 @@ private void testReceiveAndReply(long timeout) throws Exception { @Override public void doInTransactionWithoutResult(TransactionStatus status) { template.receiveAndReply((ReceiveAndReplyMessageCallback) message -> message, - (ReplyToAddressCallback) (request, reply) -> { - throw new PlannedException(); - }); + (request, reply) -> { + throw new PlannedException(); + }); } }); fail("Expected PlannedException"); @@ -1234,12 +1236,13 @@ public void testSymmetricalReceiveAndReply() throws InterruptedException { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(template.getConnectionFactory()); container.setQueues(REPLY_QUEUE); + container.setReceiveTimeout(10); container.setMessageListener(template); container.start(); int count = 10; - final Map results = new ConcurrentHashMap(); + final Map results = new ConcurrentHashMap<>(); ExecutorService executor = Executors.newFixedThreadPool(10); @@ -1342,7 +1345,8 @@ private void sendAndReceiveFastGuts(boolean tempQueue, boolean setDirectReplyToE SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(template.getConnectionFactory()); container.setQueueNames(ROUTE); - final AtomicReference replyToWas = new AtomicReference(); + container.setReceiveTimeout(10); + final AtomicReference replyToWas = new AtomicReference<>(); MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(new Object() { @SuppressWarnings("unused") @@ -1392,7 +1396,7 @@ public String handleMessage(String message) { }); messageListener.setBeforeSendReplyPostProcessors(new GZipPostProcessor()); container.setMessageListener(messageListener); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); RabbitTemplate template = createSendAndReceiveRabbitTemplate(this.template.getConnectionFactory()); @@ -1631,7 +1635,7 @@ public void testReceiveNoAutoRecovery() throws Exception { catch (AmqpException e) { e.printStackTrace(); if (e.getCause() != null - && e.getCause().getClass().equals(InterruptedException.class)) { + && e.getCause().getClass().equals(InterruptedException.class)) { Thread.currentThread().interrupt(); return; } @@ -1642,11 +1646,12 @@ public void testReceiveNoAutoRecovery() throws Exception { } } }); - System .out .println("Wait for consumer; then bounce broker; then enter after it's back up"); + PrintStream sout = System.out; + sout.println("Wait for consumer; then bounce broker; then enter after it's back up"); System.in.read(); for (int i = 0; i < 20; i++) { Properties queueProperties = admin.getQueueProperties(ROUTE); - System .out .println(queueProperties); + sout.println(queueProperties); Thread.sleep(1000); } exec.shutdownNow(); @@ -1725,10 +1730,11 @@ private class PlannedException extends RuntimeException { PlannedException() { super("Planned"); } + } @SuppressWarnings("serial") - private class TestTransactionManager extends AbstractPlatformTransactionManager { + private final class TestTransactionManager extends AbstractPlatformTransactionManager { @Override protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java index dd5a85b02f..55aabab021 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2025 the original author or 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.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; @@ -37,6 +35,8 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 1.7.6 @@ -93,6 +93,7 @@ public void testMPPsAppliedReplyContainerTests() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.config.cf()); try { container.setQueueNames(REPLIES); + container.setReceiveTimeout(10); container.setMessageListener(this.template); container.afterPropertiesSet(); container.start(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePerformanceIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePerformanceIntegrationTests.java index e4a5228a16..892a55c67f 100755 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePerformanceIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePerformanceIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -41,6 +39,8 @@ import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Gunnar Hillert diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java similarity index 88% rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java index 0ac94da528..c56bb6c571 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,7 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.ConcurrentModificationException; @@ -42,12 +30,18 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -75,12 +69,25 @@ import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.ReflectionUtils; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -91,11 +98,13 @@ * @since 1.1 * */ -@RabbitAvailable(queues = RabbitTemplatePublisherCallbacksIntegrationTests.ROUTE) -public class RabbitTemplatePublisherCallbacksIntegrationTests { +@RabbitAvailable(queues = RabbitTemplatePublisherCallbacksIntegration1Tests.ROUTE) +public class RabbitTemplatePublisherCallbacksIntegration1Tests { public static final String ROUTE = "test.queue.RabbitTemplatePublisherCallbacksIntegrationTests"; + private static final ApplicationContext APPLICATION_CONTEXT = mock(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private CachingConnectionFactory connectionFactory; @@ -118,24 +127,35 @@ public void create() { connectionFactory.setHost("localhost"); connectionFactory.setChannelCacheSize(10); connectionFactory.setPort(BrokerTestUtils.getPort()); + connectionFactory.setApplicationContext(APPLICATION_CONTEXT); + connectionFactoryWithConfirmsEnabled = new CachingConnectionFactory(); connectionFactoryWithConfirmsEnabled.setHost("localhost"); connectionFactoryWithConfirmsEnabled.setChannelCacheSize(100); connectionFactoryWithConfirmsEnabled.setPort(BrokerTestUtils.getPort()); connectionFactoryWithConfirmsEnabled.setPublisherConfirmType(ConfirmType.CORRELATED); + connectionFactoryWithConfirmsEnabled.setApplicationContext(APPLICATION_CONTEXT); + templateWithConfirmsEnabled = new RabbitTemplate(connectionFactoryWithConfirmsEnabled); + connectionFactoryWithReturnsEnabled = new CachingConnectionFactory(); connectionFactoryWithReturnsEnabled.setHost("localhost"); connectionFactoryWithReturnsEnabled.setChannelCacheSize(1); connectionFactoryWithReturnsEnabled.setPort(BrokerTestUtils.getPort()); connectionFactoryWithReturnsEnabled.setPublisherReturns(true); + connectionFactoryWithReturnsEnabled.setApplicationContext(APPLICATION_CONTEXT); + templateWithReturnsEnabled = new RabbitTemplate(connectionFactoryWithReturnsEnabled); + templateWithReturnsEnabled.setMandatory(true); + connectionFactoryWithConfirmsAndReturnsEnabled = new CachingConnectionFactory(); connectionFactoryWithConfirmsAndReturnsEnabled.setHost("localhost"); connectionFactoryWithConfirmsAndReturnsEnabled.setChannelCacheSize(100); connectionFactoryWithConfirmsAndReturnsEnabled.setPort(BrokerTestUtils.getPort()); connectionFactoryWithConfirmsAndReturnsEnabled.setPublisherConfirmType(ConfirmType.CORRELATED); connectionFactoryWithConfirmsAndReturnsEnabled.setPublisherReturns(true); + connectionFactoryWithConfirmsAndReturnsEnabled.setApplicationContext(APPLICATION_CONTEXT); + templateWithConfirmsAndReturnsEnabled = new RabbitTemplate(connectionFactoryWithConfirmsAndReturnsEnabled); templateWithConfirmsAndReturnsEnabled.setMandatory(true); } @@ -144,9 +164,19 @@ public void create() { public void cleanUp() { this.templateWithConfirmsEnabled.stop(); this.templateWithReturnsEnabled.stop(); + + this.connectionFactory.onApplicationEvent(new ContextClosedEvent(APPLICATION_CONTEXT)); this.connectionFactory.destroy(); + + this.connectionFactoryWithConfirmsEnabled.onApplicationEvent(new ContextClosedEvent(APPLICATION_CONTEXT)); this.connectionFactoryWithConfirmsEnabled.destroy(); + + this.connectionFactoryWithReturnsEnabled.onApplicationEvent(new ContextClosedEvent(APPLICATION_CONTEXT)); this.connectionFactoryWithReturnsEnabled.destroy(); + + this.connectionFactoryWithConfirmsAndReturnsEnabled.onApplicationEvent(new ContextClosedEvent(APPLICATION_CONTEXT)); + this.connectionFactoryWithConfirmsAndReturnsEnabled.destroy(); + this.executorService.shutdown(); } @@ -154,7 +184,7 @@ public void cleanUp() { public void testPublisherConfirmReceived() throws Exception { final CountDownLatch latch = new CountDownLatch(10000); final AtomicInteger acks = new AtomicInteger(); - final AtomicReference confirmCorrelation = new AtomicReference(); + final AtomicReference confirmCorrelation = new AtomicReference<>(); AtomicReference callbackThreadName = new AtomicReference<>(); this.templateWithConfirmsEnabled.setConfirmCallback((correlationData, ack, cause) -> { acks.incrementAndGet(); @@ -172,7 +202,9 @@ public Message postProcessMessage(Message message) throws AmqpException { } @Override - public Message postProcessMessage(Message message, Correlation correlation, String exch, String rk) { + public Message postProcessMessage(Message message, @Nullable Correlation correlation, + String exch, String rk) { + assertThat(exch).isEqualTo(""); assertThat(rk).isEqualTo(ROUTE); mppLatch.countDown(); @@ -189,7 +221,7 @@ public Message postProcessMessage(Message message, Correlation correlation, Stri } } catch (Throwable t) { - t.printStackTrace(); + ReflectionUtils.rethrowRuntimeException(t); } }); } @@ -203,7 +235,7 @@ public Message postProcessMessage(Message message, Correlation correlation, Stri this.templateWithConfirmsEnabled.execute(channel -> { Map listenerMap = TestUtils.getPropertyValue(((ChannelProxy) channel).getTargetChannel(), "listenerForSeq", Map.class); - await().until(() -> listenerMap.size() == 0); + await().until(listenerMap::isEmpty); return null; }); @@ -217,13 +249,15 @@ public Message postProcessMessage(Message message, Correlation correlation, Stri @Test public void testPublisherConfirmWithSendAndReceive() throws Exception { final CountDownLatch latch = new CountDownLatch(1); - final AtomicReference confirmCD = new AtomicReference(); + final AtomicReference confirmCD = new AtomicReference<>(); templateWithConfirmsEnabled.setConfirmCallback((correlationData, ack, cause) -> { confirmCD.set(correlationData); latch.countDown(); }); - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactoryWithConfirmsEnabled); + SimpleMessageListenerContainer container = + new SimpleMessageListenerContainer(this.connectionFactoryWithConfirmsEnabled); container.setQueueNames(ROUTE); + container.setReceiveTimeout(10); container.setMessageListener( new MessageListenerAdapter((ReplyingMessageListener) String::toUpperCase)); container.start(); @@ -283,7 +317,7 @@ public void testPublisherConfirmReceivedTwoTemplates() throws Exception { @Test public void testPublisherReturns() throws Exception { final CountDownLatch latch = new CountDownLatch(1); - final List returns = new ArrayList(); + final List returns = new ArrayList<>(); templateWithReturnsEnabled.setReturnsCallback((returned) -> { returns.add(returned.getMessage()); latch.countDown(); @@ -293,13 +327,13 @@ public void testPublisherReturns() throws Exception { assertThat(latch.await(1000, TimeUnit.MILLISECONDS)).isTrue(); assertThat(returns).hasSize(1); Message message = returns.get(0); - assertThat(new String(message.getBody(), "utf-8")).isEqualTo("message"); + assertThat(new String(message.getBody(), StandardCharsets.UTF_8)).isEqualTo("message"); } @Test public void testPublisherReturnsWithMandatoryExpression() throws Exception { final CountDownLatch latch = new CountDownLatch(1); - final List returns = new ArrayList(); + final List returns = new ArrayList<>(); templateWithReturnsEnabled.setReturnsCallback((returned) -> { returns.add(returned.getMessage()); latch.countDown(); @@ -311,7 +345,7 @@ public void testPublisherReturnsWithMandatoryExpression() throws Exception { assertThat(latch.await(1000, TimeUnit.MILLISECONDS)).isTrue(); assertThat(returns).hasSize(1); Message message = returns.get(0); - assertThat(new String(message.getBody(), "utf-8")).isEqualTo("message"); + assertThat(new String(message.getBody(), StandardCharsets.UTF_8)).isEqualTo("message"); } @Test @@ -320,6 +354,15 @@ public void testPublisherConfirmNotReceived() throws Exception { Connection mockConnection = mock(Connection.class); Channel mockChannel = mock(Channel.class); given(mockChannel.isOpen()).willReturn(true); + given(mockChannel.getNextPublishSeqNo()).willReturn(1L); + + CountDownLatch timeoutExceptionLatch = new CountDownLatch(1); + + given(mockChannel.waitForConfirms(anyLong())) + .willAnswer(invocation -> { + timeoutExceptionLatch.await(10, TimeUnit.SECONDS); + throw new TimeoutException(); + }); given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); given(mockConnection.isOpen()).willReturn(true); @@ -329,20 +372,26 @@ public void testPublisherConfirmNotReceived() throws Exception { .createChannel(); CachingConnectionFactory ccf = new CachingConnectionFactory(mockConnectionFactory); - ccf.setExecutor(mock(ExecutorService.class)); + ccf.setExecutor(Executors.newCachedThreadPool()); ccf.setPublisherConfirmType(ConfirmType.CORRELATED); + ccf.setChannelCacheSize(1); + ccf.setChannelCheckoutTimeout(10000); final RabbitTemplate template = new RabbitTemplate(ccf); final AtomicBoolean confirmed = new AtomicBoolean(); template.setConfirmCallback((correlationData, ack, cause) -> confirmed.set(true)); template.convertAndSend(ROUTE, (Object) "message", new CorrelationData("abc")); - Thread.sleep(5); + assertThat(template.getUnconfirmedCount()).isEqualTo(1); Collection unconfirmed = template.getUnconfirmed(-1); assertThat(template.getUnconfirmedCount()).isEqualTo(0); assertThat(unconfirmed).hasSize(1); assertThat(unconfirmed.iterator().next().getId()).isEqualTo("abc"); assertThat(confirmed.get()).isFalse(); + + timeoutExceptionLatch.countDown(); + + assertThat(ccf.createConnection().createChannel(false)).isNotNull(); } @Test @@ -413,7 +462,7 @@ public void testPublisherConfirmNotReceivedMultiThreads() throws Exception { exec.shutdown(); assertThat(exec.awaitTermination(10, TimeUnit.SECONDS)).isTrue(); ccf.destroy(); - await().until(() -> pendingConfirms.size() == 0); + await().until(pendingConfirms::isEmpty); } @Test @@ -497,7 +546,6 @@ public void testPublisherConfirmMultiple() throws Exception { /** * Tests that piggy-backed confirms (multiple=true) are distributed to the proper * template. - * @throws Exception */ @Test public void testPublisherConfirmMultipleWithTwoListeners() throws Exception { @@ -520,7 +568,7 @@ public void testPublisherConfirmMultipleWithTwoListeners() throws Exception { ccf.setPublisherConfirmType(ConfirmType.CORRELATED); final RabbitTemplate template1 = new RabbitTemplate(ccf); - final Set confirms = new HashSet(); + final Set confirms = new HashSet<>(); final CountDownLatch latch1 = new CountDownLatch(1); template1.setConfirmCallback((correlationData, ack, cause) -> { if (ack) { @@ -559,7 +607,7 @@ public void testPublisherConfirmMultipleWithTwoListeners() throws Exception { * time as adding a new pending ack to the map. Test verifies we don't * get a {@link ConcurrentModificationException}. */ - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void testConcurrentConfirms() throws Exception { ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class); @@ -623,7 +671,6 @@ public void testConcurrentConfirms() throws Exception { assertThat(waitForAll3AcksLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(acks.get()).isEqualTo(3); - channel.basicConsume("foo", false, (Map) null, null); verify(mockChannel).basicConsume("foo", false, (Map) null, null); @@ -635,8 +682,8 @@ public void testConcurrentConfirms() throws Exception { @Test public void testNackForBadExchange() throws Exception { final AtomicBoolean nack = new AtomicBoolean(true); - final AtomicReference correlation = new AtomicReference(); - final AtomicReference reason = new AtomicReference(); + final AtomicReference correlation = new AtomicReference<>(); + final AtomicReference reason = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(2); this.templateWithConfirmsEnabled.setConfirmCallback((correlationData, ack, cause) -> { nack.set(ack); @@ -645,7 +692,7 @@ public void testNackForBadExchange() throws Exception { latch.countDown(); }); Log logger = spy(TestUtils.getPropertyValue(connectionFactoryWithConfirmsEnabled, "logger", Log.class)); - final AtomicReference log = new AtomicReference(); + final AtomicReference log = new AtomicReference<>(); willAnswer(invocation -> { log.set((String) invocation.getArguments()[0]); invocation.callRealMethod(); @@ -732,7 +779,7 @@ public void testPublisherConfirmCloseConcurrencyDetectInAllPlaces() throws Excep * where the close can be detected. Run the test to verify these (and any future calls * that are added) properly emit the nacks. * - * The following will detect proper operation if any more calls are added in future. + * The following will detect proper operation if any more calls are added in the future. */ for (int i = 100; i < 110; i++) { testPublisherConfirmCloseConcurrency(i); @@ -789,7 +836,6 @@ private void testPublisherConfirmCloseConcurrency(final int closeAfter) throws E exec.shutdownNow(); } - @SuppressWarnings("unchecked") @Test public void testPublisherCallbackChannelImplCloseWithPending() throws Exception { @@ -822,7 +868,7 @@ public void testPublisherCallbackChannelImplCloseWithPending() throws Exception assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); Map pending = TestUtils.getPropertyValue(channel, "pendingConfirms", Map.class); - await().until(() -> pending.size() == 0); + await().until(pending::isEmpty); } @Test @@ -830,20 +876,20 @@ public void testWithFuture() throws Exception { RabbitAdmin admin = new RabbitAdmin(this.connectionFactory); Queue queue = QueueBuilder.nonDurable() .autoDelete() - .maxLength(1) + .maxLength(1L) .overflow(Overflow.rejectPublish) .build(); admin.declareQueue(queue); CorrelationData cd1 = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("", queue.getName(), "foo", cd1); - assertThat(cd1.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(cd1.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); CorrelationData cd2 = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("", queue.getName(), "bar", cd2); - assertThat(cd2.getFuture().get(10, TimeUnit.SECONDS).isAck()).isFalse(); + assertThat(cd2.getFuture().get(10, TimeUnit.SECONDS).ack()).isFalse(); CorrelationData cd3 = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("NO_EXCHANGE_HERE", queue.getName(), "foo", cd3); - assertThat(cd3.getFuture().get(10, TimeUnit.SECONDS).isAck()).isFalse(); - assertThat(cd3.getFuture().get().getReason()).contains("NOT_FOUND"); + assertThat(cd3.getFuture().get(10, TimeUnit.SECONDS).ack()).isFalse(); + assertThat(cd3.getFuture().get().reason()).contains("NOT_FOUND"); CorrelationData cd4 = new CorrelationData("42"); AtomicBoolean resent = new AtomicBoolean(); AtomicReference callbackThreadName = new AtomicReference<>(); @@ -855,7 +901,7 @@ public void testWithFuture() throws Exception { callbackLatch.countDown(); }); this.templateWithConfirmsAndReturnsEnabled.convertAndSend("", "NO_QUEUE_HERE", "foo", cd4); - assertThat(cd4.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(cd4.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); assertThat(callbackLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(cd4.getReturned()).isNotNull(); assertThat(resent.get()).isTrue(); @@ -863,4 +909,32 @@ public void testWithFuture() throws Exception { admin.deleteQueue(queue.getName()); } + @Test + void justReturns() throws InterruptedException { + CorrelationData correlationData = new CorrelationData(); + CountDownLatch latch = new CountDownLatch(1); + this.templateWithReturnsEnabled.setReturnsCallback(returned -> { + latch.countDown(); + }); + this.templateWithReturnsEnabled.setConfirmCallback((correlationData1, ack, cause) -> { + // has callback but factory is not enabled + }); + this.templateWithReturnsEnabled.convertAndSend("", ROUTE, "foo", correlationData); + ChannelProxy channel = (ChannelProxy) this.connectionFactoryWithReturnsEnabled.createConnection() + .createChannel(false); + assertThat(channel.getTargetChannel()) + .extracting("pendingReturns") + .asInstanceOf(InstanceOfAssertFactories.MAP) + .isEmpty(); + assertThat(channel.getTargetChannel()) + .extracting("pendingConfirms") + .asInstanceOf(InstanceOfAssertFactories.MAP) + .extracting(map -> map.values().iterator().next()) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .isEmpty(); + + this.templateWithReturnsEnabled.convertAndSend("", "___JUNK___", "foo", correlationData); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java similarity index 93% rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java index 8d46fb248f..b12f04817d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,12 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,18 +33,16 @@ import org.springframework.amqp.rabbit.junit.BrokerTestUtils; import org.springframework.amqp.rabbit.junit.RabbitAvailable; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell * @since 1.6 * */ -@RabbitAvailable(queues = { RabbitTemplatePublisherCallbacksIntegrationTests2.ROUTE, - RabbitTemplatePublisherCallbacksIntegrationTests2.ROUTE2 }) -public class RabbitTemplatePublisherCallbacksIntegrationTests2 { +@RabbitAvailable(queues = { RabbitTemplatePublisherCallbacksIntegration2Tests.ROUTE, + RabbitTemplatePublisherCallbacksIntegration2Tests.ROUTE2 }) +public class RabbitTemplatePublisherCallbacksIntegration2Tests { public static final String ROUTE = "test.queue.RabbitTemplatePublisherCallbacksIntegrationTests2"; @@ -118,13 +117,13 @@ private void routingWithConfirms(boolean listener) throws Exception { this.templateWithConfirmsEnabled.setMandatory(true); CorrelationData corr = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("", ROUTE2, "foo", corr); - assertThat(corr.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(corr.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); if (listener) { assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); } corr = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("", "bad route", "foo", corr); - assertThat(corr.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(corr.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); assertThat(corr.getReturned()).isNotNull(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests3.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java similarity index 78% rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests3.java rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java index 8e7fcef70e..2fcfc6ede4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests3.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,13 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -33,18 +32,23 @@ import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Gary Russell + * @author Artem Bilan + * * @since 2.1 * */ -@RabbitAvailable(queues = { RabbitTemplatePublisherCallbacksIntegrationTests3.QUEUE1, - RabbitTemplatePublisherCallbacksIntegrationTests3.QUEUE2, - RabbitTemplatePublisherCallbacksIntegrationTests3.QUEUE3 }) -public class RabbitTemplatePublisherCallbacksIntegrationTests3 { +@RabbitAvailable(queues = {RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE1, + RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE2, + RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE3}) +public class RabbitTemplatePublisherCallbacksIntegration3Tests { public static final String QUEUE1 = "synthetic.nack"; @@ -72,15 +76,17 @@ public void testRepublishOnNackThreadNoExchange() throws Exception { @Test public void testDeferredChannelCacheNack() throws Exception { - final CachingConnectionFactory cf = new CachingConnectionFactory( + CachingConnectionFactory cf = new CachingConnectionFactory( RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); cf.setPublisherReturns(true); cf.setPublisherConfirmType(ConfirmType.CORRELATED); - final RabbitTemplate template = new RabbitTemplate(cf); - final CountDownLatch returnLatch = new CountDownLatch(1); - final CountDownLatch confirmLatch = new CountDownLatch(1); - final AtomicInteger cacheCount = new AtomicInteger(); - final AtomicBoolean returnCalledFirst = new AtomicBoolean(); + ApplicationContext mockApplicationContext = mock(); + cf.setApplicationContext(mockApplicationContext); + RabbitTemplate template = new RabbitTemplate(cf); + CountDownLatch returnLatch = new CountDownLatch(1); + CountDownLatch confirmLatch = new CountDownLatch(1); + AtomicInteger cacheCount = new AtomicInteger(); + AtomicBoolean returnCalledFirst = new AtomicBoolean(); template.setConfirmCallback((cd, a, c) -> { cacheCount.set(TestUtils.getPropertyValue(cf, "cachedChannelsNonTransactional", List.class).size()); returnCalledFirst.set(returnLatch.getCount() == 0); @@ -104,14 +110,17 @@ public void testDeferredChannelCacheNack() throws Exception { assertThat(cacheCount.get()).isEqualTo(1); assertThat(returnCalledFirst.get()).isTrue(); assertThat(correlationData.getReturned()).isNotNull(); + cf.onApplicationEvent(new ContextClosedEvent(mockApplicationContext)); cf.destroy(); } @Test public void testDeferredChannelCacheAck() throws Exception { - final CachingConnectionFactory cf = new CachingConnectionFactory( - RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + CachingConnectionFactory cf = + new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); cf.setPublisherConfirmType(ConfirmType.CORRELATED); + ApplicationContext mockApplicationContext = mock(); + cf.setApplicationContext(mockApplicationContext); final RabbitTemplate template = new RabbitTemplate(cf); final CountDownLatch confirmLatch = new CountDownLatch(1); final AtomicInteger cacheCount = new AtomicInteger(); @@ -130,6 +139,7 @@ public void testDeferredChannelCacheAck() throws Exception { template.convertAndSend("", QUEUE2, "foo", new CorrelationData("foo")); assertThat(confirmLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(cacheCount.get()).isEqualTo(1); + cf.onApplicationEvent(new ContextClosedEvent(mockApplicationContext)); cf.destroy(); } @@ -138,6 +148,8 @@ public void testTwoSendsAndReceivesDRTMLC() throws Exception { CachingConnectionFactory cf = new CachingConnectionFactory( RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); cf.setPublisherConfirmType(ConfirmType.CORRELATED); + ApplicationContext mockApplicationContext = mock(); + cf.setApplicationContext(mockApplicationContext); RabbitTemplate template = new RabbitTemplate(cf); template.setReplyTimeout(0); final CountDownLatch confirmLatch = new CountDownLatch(2); @@ -149,6 +161,8 @@ public void testTwoSendsAndReceivesDRTMLC() throws Exception { assertThat(confirmLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(template.receive(QUEUE3, 10_000)).isNotNull(); assertThat(template.receive(QUEUE3, 10_000)).isNotNull(); + cf.onApplicationEvent(new ContextClosedEvent(mockApplicationContext)); + cf.destroy(); } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java new file mode 100644 index 0000000000..0afebb732c --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.core; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.amqp.rabbit.connection.PooledChannelConnectionFactory; +import org.springframework.amqp.rabbit.connection.SimpleRoutingConnectionFactory; +import org.springframework.amqp.rabbit.junit.BrokerTestUtils; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Leonardo Ferreira + * @since 2.4.4 + */ +@RabbitAvailable(queues = RabbitTemplateRoutingConnectionFactoryIntegrationTests.ROUTE) +class RabbitTemplateRoutingConnectionFactoryIntegrationTests { + + public static final String ROUTE = "test.queue.RabbitTemplateRoutingConnectionFactoryIntegrationTests"; + + private static RabbitTemplate rabbitTemplate; + + @BeforeAll + static void create() { + final com.rabbitmq.client.ConnectionFactory cf = new com.rabbitmq.client.ConnectionFactory(); + cf.setHost("localhost"); + cf.setPort(BrokerTestUtils.getPort()); + + CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(cf); + + cachingConnectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED); + + PooledChannelConnectionFactory pooledChannelConnectionFactory = new PooledChannelConnectionFactory(cf); + + Map connectionFactoryMap = new HashMap<>(2); + connectionFactoryMap.put("true", cachingConnectionFactory); + connectionFactoryMap.put("false", pooledChannelConnectionFactory); + + final AbstractRoutingConnectionFactory routingConnectionFactory = new SimpleRoutingConnectionFactory(); + routingConnectionFactory.setConsistentConfirmsReturns(false); + routingConnectionFactory.setDefaultTargetConnectionFactory(pooledChannelConnectionFactory); + routingConnectionFactory.setTargetConnectionFactories(connectionFactoryMap); + + rabbitTemplate = new RabbitTemplate(routingConnectionFactory); + + final Expression sendExpression = new SpelExpressionParser().parseExpression( + "messageProperties.headers['x-use-publisher-confirms'] ?: false"); + rabbitTemplate.setSendConnectionFactorySelectorExpression(sendExpression); + } + + @AfterAll + static void cleanUp() { + rabbitTemplate.destroy(); + } + + @Test + void sendWithoutConfirmsTest() { + final String payload = UUID.randomUUID().toString(); + rabbitTemplate.convertAndSend(ROUTE, (Object) payload, new CorrelationData()); + assertThat(rabbitTemplate.getUnconfirmedCount()).isZero(); + + final Message received = rabbitTemplate.receive(ROUTE, Duration.ofSeconds(3).toMillis()); + assertThat(received).isNotNull(); + final String receivedPayload = new String(received.getBody()); + + assertThat(receivedPayload).isEqualTo(payload); + } + + @Test + void sendWithConfirmsTest() throws Exception { + final String payload = UUID.randomUUID().toString(); + final Message message = MessageBuilder.withBody(payload.getBytes(StandardCharsets.UTF_8)) + .setHeader("x-use-publisher-confirms", "true").build(); + + final CorrelationData correlationData = new CorrelationData(); + rabbitTemplate.send(ROUTE, message, correlationData); + assertThat(rabbitTemplate.getUnconfirmedCount()).isEqualTo(1); + + final CorrelationData.Confirm confirm = correlationData.getFuture().get(10, TimeUnit.SECONDS); + + assertThat(confirm.ack()).isTrue(); + + final Message received = rabbitTemplate.receive(ROUTE, Duration.ofSeconds(10).toMillis()); + assertThat(received).isNotNull(); + final String receivedPayload = new String(received.getBody()); + + assertThat(receivedPayload).isEqualTo(payload); + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java index c56bf2fc86..23dfe6a348 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,24 +16,6 @@ package org.springframework.amqp.rabbit.core; -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.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -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 java.io.IOException; import java.util.Collection; import java.util.Collections; @@ -48,7 +30,20 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.AMQP.Tx.SelectOk; +import com.rabbitmq.client.AuthenticationFailureException; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownListener; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.AMQImpl; +import com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.amqp.AmqpAuthenticationException; @@ -61,7 +56,6 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.ReceiveAndReplyCallback; -import org.springframework.amqp.core.ReturnedMessage; import org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory; import org.springframework.amqp.rabbit.connection.AfterCompletionFailedException; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -71,7 +65,6 @@ import org.springframework.amqp.rabbit.connection.RabbitUtils; import org.springframework.amqp.rabbit.connection.SimpleRoutingConnectionFactory; import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnsCallback; import org.springframework.amqp.rabbit.transaction.RabbitTransactionManager; import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.amqp.utils.SerializationUtils; @@ -88,18 +81,24 @@ import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.Tx.SelectOk; -import com.rabbitmq.client.AuthenticationFailureException; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.ShutdownListener; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.client.impl.AMQImpl; -import com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk; +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.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +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; /** * @author Gary Russell @@ -371,24 +370,33 @@ public void testNoListenerAllowed2() { @SuppressWarnings("unchecked") public void testRoutingConnectionFactory() throws Exception { org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory1 = - Mockito.mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory2 = - Mockito.mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory3 = + mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory4 = + mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); Map factories = new HashMap(2); factories.put("foo", connectionFactory1); factories.put("bar", connectionFactory2); + factories.put("baz", connectionFactory3); + factories.put("qux", connectionFactory4); AbstractRoutingConnectionFactory connectionFactory = new SimpleRoutingConnectionFactory(); connectionFactory.setTargetConnectionFactories(factories); final RabbitTemplate template = new RabbitTemplate(connectionFactory); - Expression expression = new SpelExpressionParser() + Expression sendExpression = new SpelExpressionParser() .parseExpression("T(org.springframework.amqp.rabbit.core.RabbitTemplateTests)" + ".LOOKUP_KEY_COUNT.getAndIncrement() % 2 == 0 ? 'foo' : 'bar'"); - template.setSendConnectionFactorySelectorExpression(expression); - template.setReceiveConnectionFactorySelectorExpression(expression); + template.setSendConnectionFactorySelectorExpression(sendExpression); + Expression receiveExpression = new SpelExpressionParser() + .parseExpression("T(org.springframework.amqp.rabbit.core.RabbitTemplateTests)" + + ".LOOKUP_KEY_COUNT.getAndIncrement() % 2 == 0 ? 'baz' : 'qux'"); + template.setReceiveConnectionFactorySelectorExpression(receiveExpression); for (int i = 0; i < 3; i++) { try { @@ -411,8 +419,10 @@ public void testRoutingConnectionFactory() throws Exception { } } - Mockito.verify(connectionFactory1, Mockito.times(5)).createConnection(); - Mockito.verify(connectionFactory2, Mockito.times(4)).createConnection(); + Mockito.verify(connectionFactory1, times(2)).createConnection(); + Mockito.verify(connectionFactory2, times(1)).createConnection(); + Mockito.verify(connectionFactory3, times(3)).createConnection(); + Mockito.verify(connectionFactory4, times(3)).createConnection(); } @Test @@ -548,6 +558,54 @@ public void testPublisherConnWithInvoke() { verify(conn).createChannel(false); } + @Test + public void testPublisherConnWithInvokePhysicallyCloses() { + RabbitUtils.clearPhysicalCloseRequired(); + + org.springframework.amqp.rabbit.connection.ConnectionFactory cf = mock( + org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + org.springframework.amqp.rabbit.connection.ConnectionFactory pcf = mock( + org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + given(cf.getPublisherConnectionFactory()).willReturn(pcf); + given(pcf.isPublisherConfirms()).willReturn(false); + + RabbitTemplate template = new RabbitTemplate(cf); + template.setUsePublisherConnection(true); + org.springframework.amqp.rabbit.connection.Connection conn = mock( + org.springframework.amqp.rabbit.connection.Connection.class); + ChannelProxy channel = mock(ChannelProxy.class); + given(pcf.createConnection()).willReturn(conn); + given(conn.isOpen()).willReturn(true); + given(conn.createChannel(false)).willReturn(channel); + template.invoke(t -> null); + + assertThat(RabbitUtils.isPhysicalCloseRequired()).isTrue(); + } + + @Test + public void testPublisherConnWithInvokeAndPublisherConfirmations() { + RabbitUtils.clearPhysicalCloseRequired(); + + org.springframework.amqp.rabbit.connection.ConnectionFactory cf = mock( + org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + org.springframework.amqp.rabbit.connection.ConnectionFactory pcf = mock( + org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + given(cf.getPublisherConnectionFactory()).willReturn(pcf); + given(pcf.isPublisherConfirms()).willReturn(true); + + RabbitTemplate template = new RabbitTemplate(cf); + template.setUsePublisherConnection(true); + org.springframework.amqp.rabbit.connection.Connection conn = mock( + org.springframework.amqp.rabbit.connection.Connection.class); + ChannelProxy channel = mock(ChannelProxy.class); + given(pcf.createConnection()).willReturn(conn); + given(conn.isOpen()).willReturn(true); + given(conn.createChannel(false)).willReturn(channel); + template.invoke(t -> null); + + assertThat(RabbitUtils.isPhysicalCloseRequired()).isFalse(); + } + @Test public void testPublisherConnWithInvokeInTx() { org.springframework.amqp.rabbit.connection.ConnectionFactory cf = mock( @@ -569,26 +627,6 @@ public void testPublisherConnWithInvokeInTx() { verify(conn).createChannel(true); } - @SuppressWarnings("deprecation") - @Test - public void testReturnsFallback() { - RabbitTemplate template = new RabbitTemplate(); - AtomicBoolean called = new AtomicBoolean(); - template.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> { - called.set(true); - }); - ReturnsCallback cb = TestUtils.getPropertyValue(template, "returnsCallback", ReturnsCallback.class); - cb.returnedMessage(new ReturnedMessage(null, 0, null, null, null)); - assertThat(called.get()).isTrue(); - assertThatIllegalStateException().isThrownBy(() -> - template.setReturnCallback(mock(RabbitTemplate.ReturnCallback.class))); - RabbitTemplate template2 = new RabbitTemplate(); - org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback callback = - mock(org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback.class); - template2.setReturnCallback(callback); - template2.setReturnCallback(callback); - } - @Test void resourcesClearedAfterTxFails() throws IOException, TimeoutException { ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class); @@ -647,6 +685,37 @@ void resourcesClearedAfterTxFailsWithSync() throws IOException, TimeoutException ConnectionFactoryUtils.enableAfterCompletionFailureCapture(false); } + @Test + void consumerArgs() throws Exception { + ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class); + Connection mockConnection = mock(Connection.class); + Channel mockChannel = mock(Channel.class); + + given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); + given(mockConnection.isOpen()).willReturn(true); + given(mockConnection.createChannel()).willReturn(mockChannel); + willAnswer(inv -> { + Consumer consumer = inv.getArgument(3); + consumer.handleConsumeOk("tag"); + return null; + }).given(mockChannel).basicConsume(any(), anyBoolean(), anyMap(), any()); + + SingleConnectionFactory connectionFactory = new SingleConnectionFactory(mockConnectionFactory); + connectionFactory.setExecutor(mock(ExecutorService.class)); + RabbitTemplate template = new RabbitTemplate(connectionFactory); + assertThat(template.receive("foo", 1)).isNull(); + @SuppressWarnings("unchecked") + ArgumentCaptor> argsCaptor = ArgumentCaptor.forClass(Map.class); + verify(mockChannel).basicConsume(eq("foo"), eq(false), argsCaptor.capture(), any()); + assertThat(argsCaptor.getValue()).isEmpty(); + template.addConsumerArg("x-priority", 10); + assertThat(template.receive("foo", 1)).isNull(); + assertThat(argsCaptor.getValue()).containsEntry("x-priority", 10); + assertThat(template.removeConsumerArg("x-priority")).isEqualTo(10); + assertThat(template.receive("foo", 1)).isNull(); + assertThat(argsCaptor.getValue()).isEmpty(); + } + @SuppressWarnings("serial") private class TestTransactionManager extends AbstractPlatformTransactionManager { @@ -672,7 +741,7 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc } - private class DoNothingMPP implements MessagePostProcessor { + private static final class DoNothingMPP implements MessagePostProcessor { @Override public Message postProcessMessage(Message message) throws AmqpException { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/SimplePublisherConfirmsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/SimplePublisherConfirmsTests.java index 3f5714b5c4..30a298bdca 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/SimplePublisherConfirmsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/SimplePublisherConfirmsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or 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.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -28,6 +26,8 @@ import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/TransactionalEventListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/TransactionalEventListenerTests.java index 7885cace48..2691b15385 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/TransactionalEventListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/TransactionalEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,10 @@ package org.springframework.amqp.rabbit.core; -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; - import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.Connection; @@ -44,7 +40,10 @@ import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; -import com.rabbitmq.client.Channel; +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; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java new file mode 100644 index 0000000000..06e6a194fa --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java @@ -0,0 +1,180 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import com.rabbitmq.client.Channel; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @since 2.2.21 + * + */ +@SpringJUnitConfig +@RabbitAvailable(queues = { "async1", "async2" }) +@DirtiesContext +public class AsyncReplyToTests { + + @Test + void ackSingleWhenFatalSMLC(@Autowired Config config, @Autowired RabbitListenerEndpointRegistry registry, + @Autowired RabbitTemplate template, @Autowired RabbitAdmin admin) throws IOException, InterruptedException { + + template.send("async1", MessageBuilder.withBody("\"foo\"".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build()); + template.send("async1", MessageBuilder.withBody("junk".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build()); + assertThat(config.smlcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + registry.getListenerContainer("smlc").stop(); + assertThat(admin.getQueueInfo("async1").getMessageCount()).isEqualTo(1); + } + + @Test + void ackSingleWhenFatalDMLC(@Autowired Config config, @Autowired RabbitListenerEndpointRegistry registry, + @Autowired RabbitTemplate template, @Autowired RabbitAdmin admin) throws IOException, InterruptedException { + + template.send("async2", MessageBuilder.withBody("\"foo\"".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build()); + template.send("async2", MessageBuilder.withBody("junk".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build()); + assertThat(config.dmlcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + registry.getListenerContainer("dmlc").stop(); + assertThat(admin.getQueueInfo("async2").getMessageCount()).isEqualTo(0); + } + + @Configuration + @EnableRabbit + static class Config { + + volatile CountDownLatch smlcLatch = new CountDownLatch(1); + + volatile CountDownLatch dmlcLatch = new CountDownLatch(1); + + @RabbitListener(id = "smlc", queues = "async1", containerFactory = "smlcf") + CompletableFuture listen1(String in, Channel channel) { + return new CompletableFuture<>(); + } + + @RabbitListener(id = "dmlc", queues = "async2", containerFactory = "dmlcf") + CompletableFuture listen2(String in, Channel channel) { + CompletableFuture future = new CompletableFuture<>(); + future.complete("test"); + return future; + } + + @Bean + MessageConverter converter() { + return new Jackson2JsonMessageConverter(); + } + + @Bean + ConnectionFactory cf() throws IOException, TimeoutException { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + SimpleRabbitListenerContainerFactory smlcf(ConnectionFactory cf, MessageConverter converter) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf); + factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); + factory.setMessageConverter(converter); + factory.setErrorHandler(new ConditionalRejectingErrorHandler() { + + @Override + public void handleError(Throwable t) { + smlcLatch.countDown(); + super.handleError(t); + } + + }); + return factory; + } + + @Bean + DirectRabbitListenerContainerFactory dmlcf(ConnectionFactory cf, MessageConverter converter) { + DirectRabbitListenerContainerFactory factory = new DirectRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf); + factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); + factory.setMessageConverter(converter); + factory.setErrorHandler(new ConditionalRejectingErrorHandler() { + + @Override + public void handleError(Throwable t) { + dmlcLatch.countDown(); + super.handleError(t); + } + + }); + return factory; + } + + @Bean + RabbitTemplate template(ConnectionFactory cf) { + return new RabbitTemplate(cf); + } + + @Bean + RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + } +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java index c394e6bfe6..bd314d932c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -38,6 +35,9 @@ import org.springframework.amqp.rabbit.support.ActiveObjectCounter; import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * @author Dave Syer * @author Gunnar Hillert @@ -77,8 +77,8 @@ public void testTransactionalLowLevel() throws Exception { CountDownLatch latch = new CountDownLatch(2); List events = new ArrayList<>(); blockingQueueConsumer.setApplicationEventPublisher(e -> { - if (e instanceof ConsumeOkEvent) { - events.add((ConsumeOkEvent) e); + if (e instanceof ConsumeOkEvent consumeOkEvent) { + events.add(consumeOkEvent); latch.countDown(); } }); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerTests.java index 17c9d893f1..a57c643a23 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -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 java.io.IOException; import java.util.HashSet; import java.util.Map; @@ -43,6 +29,13 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -59,13 +52,19 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +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; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java index 96b67ee418..f86aeddab3 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or 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.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -42,6 +40,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @@ -124,7 +124,7 @@ private void testBrokerNamedQueue(AbstractMessageListenerContainer container, assertThat(this.message.get().getBody()).isEqualTo("foo".getBytes()); final CountDownLatch newConnectionLatch = new CountDownLatch(2); this.cf.addConnectionListener(c -> newConnectionLatch.countDown()); - this.cf.resetConnection(); + this.cf.stop(); assertThat(newConnectionLatch.await(10, TimeUnit.SECONDS)).isTrue(); String secondActualName = queue.getActualName(); assertThat(secondActualName).isNotEqualTo(firstActualName); @@ -171,7 +171,7 @@ public RabbitAdmin admin() { @Bean public AbstractMessageListenerContainer container() { - AbstractMessageListenerContainer container = new SimpleMessageListenerContainer(cf()); + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf()); container.setQueues(queue1()); container.setMessageListener(m -> { message().set(m); @@ -181,6 +181,7 @@ public AbstractMessageListenerContainer container() { container.setFailedDeclarationRetryInterval(100); container.setMissingQueuesFatal(false); container.setRecoveryInterval(100); + container.setReceiveTimeout(10); container.setAutoStartup(false); return container; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java index 2541493c7a..37df43223c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or 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.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -41,6 +39,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.1 @@ -48,6 +48,7 @@ */ @SpringJUnitConfig @RabbitAvailable +@DirtiesContext public class BrokerEventListenerTests { @Autowired diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java new file mode 100644 index 0000000000..3fcf53d8bf --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.context.support.GenericApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @since 2.4 + * + */ +@RabbitAvailable +public class ContainerAdminTests { + + @Test + void findAdminInParentContext() { + GenericApplicationContext parent = new GenericApplicationContext(); + CachingConnectionFactory cf = + new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + RabbitAdmin admin = new RabbitAdmin(cf); + parent.registerBean(RabbitAdmin.class, () -> admin); + parent.refresh(); + GenericApplicationContext child = new GenericApplicationContext(parent); + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf); + container.setReceiveTimeout(10); + child.registerBean(SimpleMessageListenerContainer.class, () -> container); + child.refresh(); + container.start(); + assertThat(TestUtils.getPropertyValue(container, "amqpAdmin")).isSameAs(admin); + container.stop(); + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java index 95dd3e2e3a..16f6acdac5 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; - import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -42,6 +38,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; + /** * @author Gary Russell * @since 1.6 @@ -63,7 +63,7 @@ public void testNoAdmin() { } catch (ApplicationContextException e) { assertThat(e.getCause().getCause()).isInstanceOf(IllegalStateException.class); - assertThat(e.getMessage()).contains("When 'mismatchedQueuesFatal' is 'true', there must be " + assertThat(e.getCause().getMessage()).contains("When 'mismatchedQueuesFatal' is 'true', there must be " + "exactly one AmqpAdmin in the context or you must inject one into this container; found: 0"); } } @@ -144,6 +144,7 @@ public ConnectionFactory connectionFactory() { public SimpleMessageListenerContainer container() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory()); container.setQueueNames(TEST_MISMATCH); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new Object() { @SuppressWarnings("unused") diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java index 6156cb534d..aec9f33cad 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 the original author or 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.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.AMQP.BasicProperties; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -30,11 +29,13 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.util.StopWatch; -import com.rabbitmq.client.AMQP.BasicProperties; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell + * @author Artem Bilan * @since 2.0 * */ @@ -44,6 +45,7 @@ public class ContainerShutDownTests { @Test public void testUninterruptibleListenerSMLC() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setReceiveTimeout(10); testUninterruptibleListener(container); } @@ -56,7 +58,6 @@ public void testUninterruptibleListenerDMLC() throws Exception { public void testUninterruptibleListener(AbstractMessageListenerContainer container) throws Exception { CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); container.setConnectionFactory(cf); - container.setShutdownTimeout(500); container.setQueueNames("test.shutdown"); final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch testEnded = new CountDownLatch(1); @@ -91,11 +92,50 @@ public void testUninterruptibleListener(AbstractMessageListenerContainer contain assertThat(channels).hasSize(2); } finally { + testEnded.countDown(); container.stop(); - assertThat(channels).hasSize(1); + cf.destroy(); + } + } + + @Test + public void consumersCorrectlyCancelledOnShutdownSMLC() throws Exception { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setReceiveTimeout(10); + consumersCorrectlyCancelledOnShutdown(container); + } + + @Test + public void consumersCorrectlyCancelledOnShutdownDMLC() throws Exception { + DirectMessageListenerContainer container = new DirectMessageListenerContainer(); + consumersCorrectlyCancelledOnShutdown(container); + } + + private void consumersCorrectlyCancelledOnShutdown(AbstractMessageListenerContainer container) + throws InterruptedException { + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + container.setConnectionFactory(cf); + container.setQueueNames("test.shutdown"); + container.setMessageListener(m -> { + }); + final CountDownLatch startLatch = new CountDownLatch(1); + container.setApplicationEventPublisher(e -> { + if (e instanceof AsyncConsumerStartedEvent) { + startLatch.countDown(); + } + }); + container.start(); + try { + assertThat(startLatch.await(30, TimeUnit.SECONDS)).isTrue(); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + container.shutdown(); + stopWatch.stop(); + assertThat(stopWatch.getTotalTimeMillis()).isLessThan(3000); + } + finally { cf.destroy(); - testEnded.countDown(); } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerUtilsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerUtilsTests.java index fd91a23942..3c456a2055 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerUtilsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 the original author or 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.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; @@ -27,6 +24,9 @@ import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Gary Russell * @since 2.1.8 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java index 7c92cf4f97..11b7feea7b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,7 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - +import java.net.UnknownHostException; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -36,8 +25,11 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; import org.aopalliance.intercept.MethodInterceptor; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -47,7 +39,10 @@ import org.mockito.ArgumentCaptor; import org.springframework.amqp.AmqpAuthenticationException; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueInformation; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -73,19 +68,30 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.backoff.FixedBackOff; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Gary Russell * @author Artem Bilan * @author Alex Panchenko + * @author Cao Weibo * * @since 2.0 * */ @RabbitAvailable(queues = { DirectMessageListenerContainerIntegrationTests.Q1, DirectMessageListenerContainerIntegrationTests.Q2, + DirectMessageListenerContainerIntegrationTests.Q3, DirectMessageListenerContainerIntegrationTests.EQ1, DirectMessageListenerContainerIntegrationTests.EQ2, DirectMessageListenerContainerIntegrationTests.DLQ1 }) @@ -98,6 +104,8 @@ public class DirectMessageListenerContainerIntegrationTests { public static final String Q2 = "testQ2.DirectMessageListenerContainerIntegrationTests"; + public static final String Q3 = "testQ3.DirectMessageListenerContainerIntegrationTests"; + public static final String EQ1 = "eventTestQ1.DirectMessageListenerContainerIntegrationTests"; public static final String EQ2 = "eventTestQ2.DirectMessageListenerContainerIntegrationTests"; @@ -176,6 +184,9 @@ public void testSimple() throws Exception { @Test public void testBadHost() throws InterruptedException { CachingConnectionFactory cf = new CachingConnectionFactory("this.host.does.not.exist"); + cf.setAddressResolver(() -> { + throw new UnknownHostException("Test Unknown Host"); + }); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setThreadNamePrefix("client-"); executor.afterPropertiesSet(); @@ -535,16 +546,20 @@ public void testRecoverDeletedQueueNoAutoDeclare(BrokerRunningSupport brokerRunn private void testRecoverDeletedQueueGuts(boolean autoDeclare, BrokerRunningSupport brokerRunning) throws Exception { CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + GenericApplicationContext context = new GenericApplicationContext(); + RabbitAdmin rabbitAdmin = new RabbitAdmin(cf); if (autoDeclare) { - GenericApplicationContext context = new GenericApplicationContext(); context.getBeanFactory().registerSingleton("foo", new Queue(Q1)); - RabbitAdmin rabbitAdmin = new RabbitAdmin(cf); - rabbitAdmin.setApplicationContext(context); - context.getBeanFactory().registerSingleton("admin", rabbitAdmin); - context.refresh(); - container.setApplicationContext(context); } - container.setAutoDeclare(autoDeclare); + else { + rabbitAdmin.setRedeclareManualDeclarations(true); + rabbitAdmin.declareQueue(new Queue(Q1)); + } + rabbitAdmin.setApplicationContext(context); + context.refresh(); + container.setApplicationContext(context); + + container.setAmqpAdmin(rabbitAdmin); container.setQueueNames(Q1, Q2); container.setConsumersPerQueue(2); container.setConsumersPerQueue(2); @@ -561,11 +576,6 @@ private void testRecoverDeletedQueueGuts(boolean autoDeclare, BrokerRunningSuppo assertThat(consumersOnQueue(Q2, 2)).isTrue(); assertThat(activeConsumerCount(container, 2)).isTrue(); assertThat(restartConsumerCount(container, 2)).isTrue(); - RabbitAdmin rabbitAdmin = new RabbitAdmin(cf); - if (!autoDeclare) { - Thread.sleep(2000); - rabbitAdmin.declareQueue(new Queue(Q1)); - } assertThat(consumersOnQueue(Q1, 2)).isTrue(); assertThat(consumersOnQueue(Q2, 2)).isTrue(); assertThat(activeConsumerCount(container, 4)).isTrue(); @@ -720,6 +730,159 @@ else if (event instanceof AsyncConsumerStartedEvent) { cf.destroy(); } + @Test + public void testMessageAckListenerWithSuccessfulAck() throws Exception { + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + final AtomicInteger calledTimes = new AtomicInteger(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + container.setQueueNames(Q1); + container.setMessageListener(new MessageListener() { + @Override + public void onMessage(Message message) { + } + }); + container.setMessageAckListener((success, deliveryTag, cause) -> { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + }); + container.start(); + RabbitTemplate rabbitTemplate = new RabbitTemplate(cf); + final int messageCount = 5; + for (int i = 0; i < messageCount; i++) { + rabbitTemplate.convertAndSend(Q1, "foo"); + } + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + container.stop(); + assertThat(calledTimes.get()).isEqualTo(messageCount); + assertThat(ackSuccess.get()).isTrue(); + assertThat(ackCause.get()).isNull(); + assertThat(ackDeliveryTag.get()).isEqualTo(messageCount); + } + + @Test + public void testMessageAckListenerWithFailedAck() throws Exception { + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + final AtomicReference called = new AtomicReference<>(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + container.setQueueNames(Q1); + container.setMessageListener(new MessageListener() { + @Override + public void onMessage(Message message) { + cf.resetConnection(); + } + }); + container.setMessageAckListener((success, deliveryTag, cause) -> { + called.set(true); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + }); + container.start(); + new RabbitTemplate(cf).convertAndSend(Q1, "foo"); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + container.stop(); + assertThat(called.get()).isTrue(); + assertThat(ackSuccess.get()).isFalse(); + assertThat(ackCause.get().getMessage()).isEqualTo("Channel closed; cannot ack/nack"); + assertThat(ackDeliveryTag.get()).isEqualTo(1); + } + + @Test + void forceStop() throws InterruptedException { + CountDownLatch latch1 = new CountDownLatch(1); + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + container.setMessageListener((ChannelAwareMessageListener) (msg, chan) -> { + latch1.await(10, TimeUnit.SECONDS); + }); + RabbitTemplate template = new RabbitTemplate(cf); + try { + container.setQueueNames(Q3); + container.setForceStop(true); + container.setShutdownTimeout(20_000L); + template.convertAndSend(Q3, "one"); + template.convertAndSend(Q3, "two"); + template.convertAndSend(Q3, "three"); + template.convertAndSend(Q3, "four"); + template.convertAndSend(Q3, "five"); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(Q3); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(5); + }); + container.start(); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(Q3); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(0); + }); + CountDownLatch latch2 = new CountDownLatch(1); + long t1 = System.currentTimeMillis(); + container.stop(() -> { + latch2.countDown(); + }); + latch1.countDown(); + assertThat(System.currentTimeMillis() - t1).isLessThan(5_000L); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(Q3); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(4); + }); + assertThat(latch2.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(container.isActive()).isFalse(); + assertThat(container.isRunning()).isFalse(); + } + finally { + container.stop(); + } + } + + @Test + public void testMessageAckListenerWithBatchAck() throws Exception { + final AtomicInteger calledTimes = new AtomicInteger(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + final int messageCount = 5; + final CountDownLatch latch = new CountDownLatch(1); + container.setQueueNames(Q1); + container.setMessagesPerAck(messageCount); + container.setMessageListener(message -> { + }); + container.setMessageAckListener((success, deliveryTag, cause) -> { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + }); + container.start(); + RabbitTemplate rabbitTemplate = new RabbitTemplate(cf); + for (int i = 0; i < messageCount; i++) { + rabbitTemplate.convertAndSend(Q1, "foo"); + } + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + container.stop(); + assertThat(calledTimes.get()).isEqualTo(1); + assertThat(ackSuccess.get()).isTrue(); + assertThat(ackCause.get()).isNull(); + assertThat(ackDeliveryTag.get()).isEqualTo(messageCount); + } + private boolean consumersOnQueue(String queue, int expected) throws Exception { await().with().pollDelay(Duration.ZERO).atMost(Duration.ofSeconds(60)) .until(() -> admin.getQueueProperties(queue), diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java index 6d5dd3b8ec..f3a710ab21 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -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 java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -38,6 +23,12 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -47,12 +38,20 @@ import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -360,6 +359,29 @@ public void testMonitorCancelsAfterTargetChannelChanges() throws Exception { container.stop(); } + @Test + void monitorTaskThreadName() { + DirectMessageListenerContainer container = new DirectMessageListenerContainer(mock(ConnectionFactory.class)); + assertThat(container.getListenerId()).isEqualTo("not.a.Spring.bean"); + container.setBeanName("aBean"); + assertThat(container.getListenerId()).isEqualTo("aBean"); + container.setListenerId("id"); + assertThat(container.getListenerId()).isEqualTo("id"); + container.afterPropertiesSet(); + assertThat(container).extracting("taskScheduler") + .extracting("threadNamePrefix") + .asString() + .startsWith("id-consumerMonitor"); + + container = new DirectMessageListenerContainer(mock(ConnectionFactory.class)); + container.setBeanName("aBean"); + container.afterPropertiesSet(); + assertThat(container).extracting("taskScheduler") + .extracting("threadNamePrefix") + .asString() + .startsWith("aBean-consumerMonitor"); + } + private Envelope envelope(long tag) { return new Envelope(tag, false, "", ""); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainerTests.java index 20bd2e6b98..99de689327 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.time.Duration; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.GetResponse; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Address; @@ -34,9 +34,8 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.GetResponse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * DirectReplyToMessageListenerContainer Tests. @@ -57,7 +56,8 @@ public void testReleaseConsumerRace() throws Exception { final CountDownLatch latch = new CountDownLatch(1); // Populate void MessageListener for wrapping in the DirectReplyToMessageListenerContainer - container.setMessageListener(m -> { }); + container.setMessageListener(m -> { + }); // Extract actual ChannelAwareMessageListener from container // with the inUseConsumerChannels.remove(channel); operation diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java index 729a66e825..f5639fce06 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or 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.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -38,8 +36,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.1 @@ -47,6 +48,7 @@ */ @RabbitAvailable @SpringJUnitConfig +@DirtiesContext public class DlqExpiryTests { @Autowired diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java index 2d9bcf0947..1fe24db479 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.willDoNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; - import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; @@ -38,6 +31,13 @@ import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; import org.springframework.messaging.handler.annotation.support.MethodArgumentTypeMismatchException; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + /** * @author Gary Russell * @author Artem Bilan @@ -127,7 +127,7 @@ private void doTest(Throwable cause) { new MessageProperties()))); } - private static class Foo { + private static final class Foo { @SuppressWarnings("unused") public void foo(String foo) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java index e1fb3e6941..07819e5b44 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or 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,41 @@ package org.springframework.amqp.rabbit.listener; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.context.ApplicationEventPublisher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell + * @author Thomas Badie * @since 2.0 * */ @@ -32,4 +63,92 @@ protected AbstractMessageListenerContainer createContainer(AbstractConnectionFac return container; } + + @Test + public void testMessageListenerTxFail() throws Exception { + ConnectionFactoryUtils.enableAfterCompletionFailureCapture(true); + ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class); + Connection mockConnection = mock(Connection.class); + final Channel mockChannel = mock(Channel.class); + given(mockChannel.isOpen()).willReturn(true); + given(mockChannel.txSelect()).willReturn(mock(AMQP.Tx.SelectOk.class)); + final AtomicReference commitLatch = new AtomicReference<>(new CountDownLatch(1)); + String exceptionMessage = "Failed to commit."; + willAnswer(invocation -> { + commitLatch.get().countDown(); + throw new IllegalStateException(exceptionMessage); + }).given(mockChannel).txCommit(); + + final CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(mockConnectionFactory); + cachingConnectionFactory.setExecutor(mock(ExecutorService.class)); + given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); + given(mockConnection.isOpen()).willReturn(true); + + willAnswer(invocation -> mockChannel).given(mockConnection).createChannel(); + + final AtomicReference consumer = new AtomicReference(); + final CountDownLatch consumerLatch = new CountDownLatch(1); + + willAnswer(invocation -> { + consumer.set(invocation.getArgument(6)); + consumerLatch.countDown(); + return "consumerTag"; + }).given(mockChannel) + .basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), anyMap(), + any(Consumer.class)); + + + final CountDownLatch latch = new CountDownLatch(1); + AbstractMessageListenerContainer container = createContainer(cachingConnectionFactory); + container.setMessageListener(message -> { + RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory); + rabbitTemplate.setChannelTransacted(true); + // should use same channel as container + rabbitTemplate.convertAndSend("foo", "bar", "baz"); + latch.countDown(); + }); + container.setQueueNames("queue"); + container.setChannelTransacted(true); + container.setShutdownTimeout(100); + DummyTxManager transactionManager = new DummyTxManager(); + container.setTransactionManager(transactionManager); + ApplicationEventPublisher applicationEventPublisher = mock(ApplicationEventPublisher.class); + final CountDownLatch applicationEventPublisherLatch = new CountDownLatch(1); + willAnswer(invocation -> { + if (invocation.getArgument(0) instanceof ListenerContainerConsumerFailedEvent) { + applicationEventPublisherLatch.countDown(); + } + return null; + }).given(applicationEventPublisher).publishEvent(any()); + + container.setApplicationEventPublisher(applicationEventPublisher); + container.afterPropertiesSet(); + container.start(); + assertThat(consumerLatch.await(10, TimeUnit.SECONDS)).isTrue(); + + consumer.get().handleDelivery("qux", + new Envelope(1, false, "foo", "bar"), new AMQP.BasicProperties(), + new byte[] { 0 }); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + + verify(mockConnection, times(1)).createChannel(); + assertThat(commitLatch.get().await(10, TimeUnit.SECONDS)).isTrue(); + verify(mockChannel).basicAck(anyLong(), anyBoolean()); + verify(mockChannel).txCommit(); + + assertThat(applicationEventPublisherLatch.await(10, TimeUnit.SECONDS)).isTrue(); + verify(applicationEventPublisher).publishEvent(any(ListenerContainerConsumerFailedEvent.class)); + + ArgumentCaptor argumentCaptor + = ArgumentCaptor.forClass(ListenerContainerConsumerFailedEvent.class); + verify(applicationEventPublisher).publishEvent(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().getThrowable()).hasCauseInstanceOf(IllegalStateException.class); + assertThat(argumentCaptor.getValue().getThrowable()) + .isNotNull().extracting(Throwable::getCause) + .isNotNull().extracting(Throwable::getMessage).isEqualTo(exceptionMessage); + container.stop(); + } + + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java index ac4e0c86bc..45beb6ab0f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -37,6 +24,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.mockito.stubbing.Answer; @@ -60,15 +53,22 @@ import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell + * @author Thomas Badie * @since 1.1.2 * */ @@ -213,7 +213,6 @@ public void testMessageListener() throws Exception { transactionManager.rolledBack = false; transactionManager.latch = new CountDownLatch(1); - container.setAfterReceivePostProcessors(m -> null); container.setMessageListener(m -> { // NOSONAR }); @@ -758,7 +757,7 @@ public void testMessageListenerWithRabbitTxManager() throws Exception { container.stop(); } - private Answer ensureOneChannelAnswer(final Channel onlyChannel, + protected Answer ensureOneChannelAnswer(final Channel onlyChannel, final AtomicReference tooManyChannels) { final AtomicBoolean done = new AtomicBoolean(); return invocation -> { @@ -776,7 +775,7 @@ private Answer ensureOneChannelAnswer(final Channel onlyChannel, protected abstract AbstractMessageListenerContainer createContainer(AbstractConnectionFactory connectionFactory); @SuppressWarnings("serial") - private static class DummyTxManager extends AbstractPlatformTransactionManager { + protected static class DummyTxManager extends AbstractPlatformTransactionManager { private volatile boolean committed; @@ -804,6 +803,30 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc this.rolledBack = true; this.latch.countDown(); } + + public boolean isCommitted() { + return committed; + } + + public void setCommitted(boolean committed) { + this.committed = committed; + } + + public boolean isRolledBack() { + return rolledBack; + } + + public void setRolledBack(boolean rolledBack) { + this.rolledBack = rolledBack; + } + + public CountDownLatch getLatch() { + return latch; + } + + public void setLatch(CountDownLatch latch) { + this.latch = latch; + } } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java index 7f5056fee5..ffb379973b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.util.Arrays; import java.util.UUID; @@ -46,6 +43,9 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * NOTE: This class is referenced in the reference documentation; if it is changed/moved, be * sure to update that documentation. @@ -170,6 +170,7 @@ public SimpleMessageListenerContainer replyListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(replyQueue()); + container.setReceiveTimeout(10); container.setMessageListener(fixedReplyQRabbitTemplate()); return container; } @@ -182,6 +183,7 @@ public SimpleMessageListenerContainer serviceListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(requestQueue()); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new PojoListener())); return container; } @@ -194,6 +196,7 @@ public SimpleMessageListenerContainer replyListenerContainerWrongQueue() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(replyQueue()); + container.setReceiveTimeout(10); container.setMessageListener(fixedReplyQRabbitTemplateWrongQueue()); container.setAutoStartup(false); return container; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ListenFromAutoDeleteQueueTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ListenFromAutoDeleteQueueTests.java index 88be38d8f8..a560ceb652 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ListenFromAutoDeleteQueueTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ListenFromAutoDeleteQueueTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -46,6 +35,17 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedSMLCTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedSMLCTests.java index 043679f74b..60ebe309c7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedSMLCTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedSMLCTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,8 @@ public class LocallyTransactedSMLCTests extends LocallyTransactedTests { @Override protected AbstractMessageListenerContainer createContainer(AbstractConnectionFactory connectionFactory) { - AbstractMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setReceiveTimeout(10); return container; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java index 9ed4a913e5..97ad02d83a 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -36,6 +23,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; @@ -50,12 +43,18 @@ import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.beans.DirectFieldAccessor; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -180,7 +179,6 @@ public void testMessageListener() throws Exception { verify(onlyChannel, times(2)).basicNack(anyLong(), anyBoolean(), anyBoolean()); verify(onlyChannel, times(2)).txRollback(); - container.setAfterReceivePostProcessors(m -> null); container.setMessageListener(m -> { // NOSONAR }); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java index 627c8a7670..ec4ae38f9b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,11 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.contains; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.BeforeEach; @@ -63,7 +51,17 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.util.ErrorHandler; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Dave Syer @@ -101,6 +99,7 @@ public void testErrorHandlerThrowsARADRE() throws Exception { RabbitTemplate template = this.createTemplate(1); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(template.getConnectionFactory()); container.setQueues(QUEUE); + container.setReceiveTimeout(10); final CountDownLatch messageReceived = new CountDownLatch(1); final CountDownLatch spiedQLogger = new CountDownLatch(1); final CountDownLatch errorHandled = new CountDownLatch(1); @@ -108,7 +107,7 @@ public void testErrorHandlerThrowsARADRE() throws Exception { errorHandled.countDown(); throw new AmqpRejectAndDontRequeueException("foo", t); }); - container.setMessageListener((MessageListener) message -> { + container.setMessageListener(message -> { try { messageReceived.countDown(); spiedQLogger.await(10, TimeUnit.SECONDS); @@ -125,16 +124,10 @@ public void testErrorHandlerThrowsARADRE() throws Exception { new DirectFieldAccessor(container).setPropertyValue("logger", logger); template.convertAndSend(QUEUE.getName(), "baz"); assertThat(messageReceived.await(10, TimeUnit.SECONDS)).isTrue(); - Object consumer = TestUtils.getPropertyValue(container, "consumers", Set.class) - .iterator().next(); - Log qLogger = spy(TestUtils.getPropertyValue(consumer, "logger", Log.class)); - willReturn(true).given(qLogger).isDebugEnabled(); - new DirectFieldAccessor(consumer).setPropertyValue("logger", qLogger); spiedQLogger.countDown(); assertThat(errorHandled.await(10, TimeUnit.SECONDS)).isTrue(); container.stop(); verify(logger, never()).warn(contains("Consumer raised exception"), any(Throwable.class)); - verify(qLogger).debug(contains("Rejecting messages (requeue=false)")); ((DisposableBean) template.getConnectionFactory()).destroy(); } @@ -256,8 +249,8 @@ private void testRejectingErrorHandler(RabbitTemplate template, AbstractMessageL // Verify that the exception strategy has access to the message final AtomicReference failed = new AtomicReference(); ConditionalRejectingErrorHandler eh = new ConditionalRejectingErrorHandler(t -> { - if (t instanceof ListenerExecutionFailedException) { - failed.set(((ListenerExecutionFailedException) t).getFailedMessage()); + if (t instanceof ListenerExecutionFailedException exception) { + failed.set(exception.getFailedMessage()); } return t instanceof ListenerExecutionFailedException && t.getCause() instanceof MessageConversionException; @@ -344,7 +337,9 @@ private RabbitTemplate createTemplate(int concurrentConsumers) { // Helper classes // /////////////// public static class PojoThrowingExceptionListener { + private final CountDownLatch latch; + private final Throwable exception; public PojoThrowingExceptionListener(CountDownLatch latch, Throwable exception) { @@ -362,10 +357,13 @@ public void handleMessage(String value) throws Throwable { latch.countDown(); } } + } public static class ThrowingExceptionListener implements MessageListener { + private final CountDownLatch latch; + private final RuntimeException exception; public ThrowingExceptionListener(CountDownLatch latch, RuntimeException exception) { @@ -390,10 +388,13 @@ public void onMessage(Message message) { latch.countDown(); } } + } public static class ThrowingExceptionChannelAwareListener implements ChannelAwareMessageListener { + private final CountDownLatch latch; + private final Exception exception; public ThrowingExceptionChannelAwareListener(CountDownLatch latch, Exception exception) { @@ -418,6 +419,7 @@ public void onMessage(Message message, Channel channel) throws Exception { latch.countDown(); } } + } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java index 6aeb1ea4b1..2332ea590d 100755 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.awaitility.Awaitility.await; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.spy; - import java.net.UnknownHostException; import java.util.Set; import java.util.concurrent.BlockingQueue; @@ -31,6 +24,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.DnsRecordIpAddressResolver; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; @@ -38,7 +32,6 @@ import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.AcknowledgeMode; -import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -59,7 +52,12 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.context.junit.jupiter.DisabledIf; -import com.rabbitmq.client.DnsRecordIpAddressResolver; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.spy; /** * @author Dave Syer @@ -71,9 +69,9 @@ */ @RabbitAvailable(queues = MessageListenerContainerLifecycleIntegrationTests.TEST_QUEUE) @LongRunning -@LogLevels(classes = { RabbitTemplate.class, - SimpleMessageListenerContainer.class, BlockingQueueConsumer.class, - MessageListenerContainerLifecycleIntegrationTests.class }, level = "INFO") +@LogLevels(classes = {RabbitTemplate.class, + SimpleMessageListenerContainer.class, BlockingQueueConsumer.class, + MessageListenerContainerLifecycleIntegrationTests.class}, level = "INFO") public class MessageListenerContainerLifecycleIntegrationTests { public static final String TEST_QUEUE = "test.queue.MessageListenerContainerLifecycleIntegrationTests"; @@ -84,6 +82,7 @@ public class MessageListenerContainerLifecycleIntegrationTests { private enum TransactionMode { ON, OFF, PREFETCH, PREFETCH_NO_TX; + public boolean isTransactional() { return this != OFF && this != PREFETCH_NO_TX; } @@ -103,6 +102,7 @@ public int getTxSize() { private enum Concurrency { LOW(1), HIGH(5); + private final int value; Concurrency(int value) { @@ -116,6 +116,7 @@ public int value() { private enum MessageCount { LOW(1), MEDIUM(20), HIGH(500); + private final int value; MessageCount(int value) { @@ -199,7 +200,7 @@ public void testBadCredentials() throws Exception { cf.setUsername("foo"); final CachingConnectionFactory connectionFactory = new CachingConnectionFactory(cf); assertThatExceptionOfType(AmqpIllegalStateException.class).isThrownBy(() -> - doTest(MessageCount.LOW, Concurrency.LOW, TransactionMode.OFF, template, connectionFactory)) + doTest(MessageCount.LOW, Concurrency.LOW, TransactionMode.OFF, template, connectionFactory)) .withCauseExactlyInstanceOf(FatalListenerStartupException.class); ((DisposableBean) template.getConnectionFactory()).destroy(); } @@ -334,7 +335,7 @@ public void testShutDownWithPrefetch() throws Exception { final AtomicInteger received = new AtomicInteger(); final CountDownLatch awaitConsumeFirst = new CountDownLatch(5); final CountDownLatch awaitConsumeSecond = new CountDownLatch(10); - container.setMessageListener((MessageListener) message -> { + container.setMessageListener(message -> { try { awaitStart1.countDown(); prefetched.await(10, TimeUnit.SECONDS); @@ -353,6 +354,7 @@ public void testShutDownWithPrefetch() throws Exception { container.setPrefetchCount(5); container.setQueueNames(queue.getName()); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); @@ -364,7 +366,7 @@ public void testShutDownWithPrefetch() throws Exception { .getPropertyValue(container, "consumers"); await().until(() -> { if (consumers.size() > 0 - && TestUtils.getPropertyValue(consumers.iterator().next(), "queue", BlockingQueue.class).size() > 3) { + && TestUtils.getPropertyValue(consumers.iterator().next(), "queue", BlockingQueue.class).size() > 3) { prefetched.countDown(); return true; } @@ -411,6 +413,7 @@ public void testSimpleMessageListenerContainerStoppedWithoutWarn() throws Except DirectFieldAccessor dfa = new DirectFieldAccessor(container); dfa.setPropertyValue("logger", log); container.setQueues(queue); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter()); container.afterPropertiesSet(); container.start(); @@ -484,7 +487,8 @@ public ConnectionFactory connectionFactory() { public SimpleMessageListenerContainer container() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory()); container.setQueues(queue); - container.setMessageListener((MessageListener) message -> { + container.setReceiveTimeout(10); + container.setMessageListener(message -> { try { consumerLatch().countDown(); Thread.sleep(500); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java index 343fc7ff70..8e1e64ca62 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -36,15 +34,18 @@ import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.support.converter.SimpleMessageConverter; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Mark Fisher * @author Gunnar Hillert * @author Gary Russell + * @author Artem Bilan */ -@RabbitAvailable(queues = { MessageListenerContainerMultipleQueueIntegrationTests.TEST_QUEUE_1, - MessageListenerContainerMultipleQueueIntegrationTests.TEST_QUEUE_2 }) -@LogLevels(level = "INFO", classes = { RabbitTemplate.class, - SimpleMessageListenerContainer.class, BlockingQueueConsumer.class }) +@RabbitAvailable(queues = {MessageListenerContainerMultipleQueueIntegrationTests.TEST_QUEUE_1, + MessageListenerContainerMultipleQueueIntegrationTests.TEST_QUEUE_2}) +@LogLevels(level = "INFO", classes = {RabbitTemplate.class, + SimpleMessageListenerContainer.class, BlockingQueueConsumer.class}) public class MessageListenerContainerMultipleQueueIntegrationTests { public static final String TEST_QUEUE_1 = "test.queue.1.MessageListenerContainerMultipleQueueIntegrationTests"; @@ -77,7 +78,6 @@ public void testMultipleQueueNamesWithConcurrentConsumers() { doTest(3, container -> container.setQueueNames(queue1.getName(), queue2.getName())); } - private void doTest(int concurrentConsumers, ContainerConfigurer configurer) { int messageCount = 10; RabbitTemplate template = new RabbitTemplate(); @@ -90,8 +90,8 @@ private void doTest(int concurrentConsumers, ContainerConfigurer configurer) { messageConverter.setCreateMessageIds(true); template.setMessageConverter(messageConverter); for (int i = 0; i < messageCount; i++) { - template.convertAndSend(queue1.getName(), Integer.valueOf(i)); - template.convertAndSend(queue2.getName(), Integer.valueOf(i)); + template.convertAndSend(queue1.getName(), i); + template.convertAndSend(queue2.getName(), i); } final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); final CountDownLatch latch = new CountDownLatch(messageCount * 2); @@ -100,6 +100,7 @@ private void doTest(int concurrentConsumers, ContainerConfigurer configurer) { container.setAcknowledgeMode(AcknowledgeMode.AUTO); container.setChannelTransacted(true); container.setConcurrentConsumers(concurrentConsumers); + container.setReceiveTimeout(10); configurer.configure(container); container.afterPropertiesSet(); container.start(); @@ -118,17 +119,15 @@ private void doTest(int concurrentConsumers, ContainerConfigurer configurer) { container.shutdown(); assertThat(container.getActiveConsumerCount()).isEqualTo(0); } - assertThat(template.receiveAndConvert(queue1.getName())).isNull(); - assertThat(template.receiveAndConvert(queue2.getName())).isNull(); - connectionFactory.destroy(); } @FunctionalInterface private interface ContainerConfigurer { + void configure(SimpleMessageListenerContainer container); - } + } @SuppressWarnings("unused") private static class PojoListener { @@ -149,6 +148,7 @@ public void handleMessage(int value) throws Exception { public int getCount() { return count.get(); } + } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java index 5663575341..e958d10469 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -49,6 +46,9 @@ import org.springframework.retry.policy.MapRetryContextCache; import org.springframework.retry.support.RetryTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + /** * @author Dave Syer * @author Gary Russell @@ -116,7 +116,7 @@ private void doTestRetryWithBatchListener(boolean stateful) throws Exception { container.setBatchSize(2); final CountDownLatch latch = new CountDownLatch(1); - container.setAdviceChain(new Advice[] { createRetryInterceptor(latch, stateful, true) }); + container.setAdviceChain(createRetryInterceptor(latch, stateful, true)); container.setQueueNames(queue.getName()); container.setReceiveTimeout(50); @@ -252,7 +252,7 @@ private void doTestRetry(int messageCount, int txSize, int failFrequency, int co container.setConcurrentConsumers(concurrentConsumers); final CountDownLatch latch = new CountDownLatch(failedMessageCount); - container.setAdviceChain(new Advice[] { createRetryInterceptor(latch, stateful) }); + container.setAdviceChain(createRetryInterceptor(latch, stateful)); container.setQueueNames(queue.getName()); container.setReceiveTimeout(50); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java index fb097e447c..c2b02ff07b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; - import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -34,13 +25,6 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import org.springframework.transaction.support.TransactionSynchronizationManager; - import com.rabbitmq.client.AMQP; import com.rabbitmq.client.AMQP.Tx.SelectOk; import com.rabbitmq.client.Channel; @@ -48,6 +32,21 @@ import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Consumer; import com.rabbitmq.client.Envelope; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; /** * @author Gary Russell @@ -88,6 +87,7 @@ void resourcesClearedAfterTxFails() throws IOException, TimeoutException, Interr mlc.setQueueNames("foo"); mlc.setTaskExecutor(exec); mlc.setChannelTransacted(true); + mlc.setReceiveTimeout(10); CountDownLatch latch2 = new CountDownLatch(1); mlc.setMessageListener(msg -> { template.convertAndSend("foo", "bar"); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java index beb265e8d5..fe8e786ff5 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,20 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -39,12 +40,14 @@ import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.beans.factory.DisposableBean; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * @author Dave Syer * @author Gunnar Hillert * @author Gary Russell + * @author Artem Bilan * * @since 1.0 * @@ -56,7 +59,7 @@ public class MessageListenerManualAckIntegrationTests { public static final String TEST_QUEUE = "test.queue.MessageListenerManualAckIntegrationTests"; - private static Log logger = LogFactory.getLog(MessageListenerManualAckIntegrationTests.class); + private static final Log logger = LogFactory.getLog(MessageListenerManualAckIntegrationTests.class); private final Queue queue = new Queue(TEST_QUEUE); @@ -121,6 +124,26 @@ public void testListenerWithManualAckTransactional() throws Exception { assertThat(template.receiveAndConvert(queue.getName())).isNull(); } + @Test + public void immediateIsAckedForManual() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + container = createContainer(new ImmediateTestListener(latch)); + container.setEnforceImmediateAckForManual(true); + + template.convertAndSend(queue.getName(), "test data"); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + + container.stop(); + + Channel channel = template.getConnectionFactory().createConnection().createChannel(false); + + await().untilAsserted(() -> assertThat(channel.consumerCount(queue.getName())).isEqualTo(0)); + assertThat(channel.messageCount(queue.getName())).isEqualTo(0); + + channel.close(); + } + private SimpleMessageListenerContainer createContainer(Object listener) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(template.getConnectionFactory()); container.setMessageListener(new MessageListenerAdapter(listener)); @@ -130,6 +153,7 @@ private SimpleMessageListenerContainer createContainer(Object listener) { container.setConcurrentConsumers(concurrentConsumers); container.setChannelTransacted(transactional); container.setAcknowledgeMode(AcknowledgeMode.MANUAL); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); return container; @@ -159,4 +183,23 @@ public void onMessage(Message message, Channel channel) throws Exception { } } + static class ImmediateTestListener implements MessageListener { + + private final CountDownLatch latch; + + ImmediateTestListener(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void onMessage(Message message) { + try { + throw new ImmediateAcknowledgeAmqpException("intentional"); + } + finally { + this.latch.countDown(); + } + } + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryCachingConnectionIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryCachingConnectionIntegrationTests.java index 8cf6ffff0c..885a4185fc 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryCachingConnectionIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryCachingConnectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.awaitility.Awaitility.with; - import java.time.Duration; import java.util.Collections; import java.util.HashSet; @@ -29,6 +25,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -56,7 +53,9 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.support.GenericApplicationContext; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.with; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryRepeatIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryRepeatIntegrationTests.java index 2ea14ec279..0081c73a78 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryRepeatIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryRepeatIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,12 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -46,7 +45,7 @@ import org.springframework.amqp.rabbit.listener.exception.FatalListenerExecutionException; import org.springframework.beans.factory.DisposableBean; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; /** * Long-running test created to facilitate profiling of SimpleMessageListenerContainer. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java index adc1332cd5..7fe5d3dbc6 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,10 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -39,7 +38,7 @@ import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.beans.factory.DisposableBean; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Dave Syer @@ -50,8 +49,8 @@ * */ @RabbitAvailable(queues = MessageListenerTxSizeIntegrationTests.TEST_QUEUE) -@LogLevels(level = "ERROR", classes = { RabbitTemplate.class, - SimpleMessageListenerContainer.class, BlockingQueueConsumer.class }) +@LogLevels(level = "ERROR", classes = {RabbitTemplate.class, + SimpleMessageListenerContainer.class, BlockingQueueConsumer.class}) public class MessageListenerTxSizeIntegrationTests { public static final String TEST_QUEUE = "test.queue.MessageListenerTxSizeIntegrationTests"; @@ -132,6 +131,7 @@ private SimpleMessageListenerContainer createContainer(Object listener) { container.setConcurrentConsumers(concurrentConsumers); container.setChannelTransacted(transactional); container.setAcknowledgeMode(AcknowledgeMode.AUTO); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); return container; @@ -139,7 +139,7 @@ private SimpleMessageListenerContainer createContainer(Object listener) { public class TestListener implements ChannelAwareMessageListener { - private final ThreadLocal count = new ThreadLocal(); + private final ThreadLocal count = new ThreadLocal<>(); private final CountDownLatch latch; @@ -174,6 +174,7 @@ public void onMessage(Message message, Channel channel) throws Exception { latch.countDown(); } } + } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java index 24463d19a3..254753d118 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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,16 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.AdditionalMatchers.aryEq; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.io.Serializable; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -44,11 +39,11 @@ import org.springframework.amqp.rabbit.test.MessageTestUtils; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.AmqpMessageHeaderAccessor; -import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.utils.SerializationUtils; import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; @@ -60,9 +55,14 @@ import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; - +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Stephane Nicoll @@ -79,22 +79,22 @@ public class MethodRabbitListenerEndpointTests { public String testName; + @BeforeAll + static void setUp() { + System.setProperty("spring.amqp.deserialization.trust.all", "true"); + } + + @AfterAll + static void tearDown() { + System.setProperty("spring.amqp.deserialization.trust.all", "false"); + } + @BeforeEach public void setup(TestInfo info) { initializeFactory(factory); this.testName = info.getTestMethod().get().getName(); } - @Test - public void createMessageListenerNoFactory(TestInfo info) { - MethodRabbitListenerEndpoint endpoint = new MethodRabbitListenerEndpoint(); - endpoint.setBean(this); - endpoint.setMethod(info.getTestMethod().get()); - - assertThatIllegalStateException() - .isThrownBy(() -> endpoint.createMessageListener(container)); - } - @Test public void createMessageListener(TestInfo info) { MethodRabbitListenerEndpoint endpoint = new MethodRabbitListenerEndpoint(); @@ -241,7 +241,6 @@ public void processAndReplyWithMessage() throws Exception { org.springframework.amqp.core.Message message = MessageTestUtils.createTextMessage(body, new MessageProperties()); - processAndReply(listener, message, "fooQueue", "", false, null); assertDefaultListenerMethodInvocation(); } @@ -279,7 +278,6 @@ public void processAndReplyUsingReplyTo() throws Exception { properties.setReplyToAddress(replyTo); org.springframework.amqp.core.Message message = MessageTestUtils.createTextMessage(body, properties); - processAndReply(listener, message, "replyToQueue", "myRouting", true, null); assertDefaultListenerMethodInvocation(); } @@ -329,8 +327,8 @@ public void noSendToValue() throws Exception { @Test public void invalidSendTo() { assertThatIllegalStateException() - .isThrownBy(() -> createDefaultInstance(String.class)) - .withMessageMatching(".*firstDestination, secondDestination.*"); + .isThrownBy(() -> createDefaultInstance(String.class)) + .withMessageMatching(".*firstDestination, secondDestination.*"); } @Test @@ -358,7 +356,7 @@ public void validatePayloadInvalid() { Channel channel = mock(Channel.class); assertThatThrownBy(() -> listener.onMessage(MessageTestUtils.createTextMessage("invalid value"), channel)) - .isInstanceOf(ListenerExecutionFailedException.class); + .isInstanceOf(ListenerExecutionFailedException.class); } @@ -371,9 +369,9 @@ public void invalidPayloadType() { // test is not a valid integer assertThatThrownBy(() -> listener.onMessage(MessageTestUtils.createTextMessage("test"), channel)) - .isInstanceOf(ListenerExecutionFailedException.class) - .hasCauseExactlyInstanceOf(org.springframework.messaging.converter.MessageConversionException.class) - .hasMessageContaining(getDefaultListenerMethod(Integer.class).toGenericString()); // ref to method + .isInstanceOf(ListenerExecutionFailedException.class) + .hasCauseExactlyInstanceOf(org.springframework.messaging.converter.MessageConversionException.class) + .hasMessageContaining(getDefaultListenerMethod(Integer.class).toGenericString()); // ref to method } @Test @@ -383,9 +381,9 @@ public void invalidMessagePayloadType() { // Message as Message assertThatThrownBy(() -> listener.onMessage(MessageTestUtils.createTextMessage("test"), channel)) - .extracting(t -> t.getCause()) - .isInstanceOfAny(MethodArgumentTypeMismatchException.class, - org.springframework.messaging.converter.MessageConversionException.class); + .extracting(t -> t.getCause()) + .isInstanceOfAny(MethodArgumentTypeMismatchException.class, + org.springframework.messaging.converter.MessageConversionException.class); } private MessagingMessageListenerAdapter createInstance( @@ -434,6 +432,7 @@ private void initializeFactory(DefaultMessageHandlerMethodFactory methodFactory) private Validator testValidator(final String invalidValue) { return new Validator() { + @Override public boolean supports(Class clazz) { return String.class.isAssignableFrom(clazz); @@ -465,7 +464,7 @@ public void resolveGenericMessage(Message message) { assertThat(message.getPayload()).as("Wrong message payload").isEqualTo("test"); } - public void resolveHeaderAndPayload(@Payload String content, @Header int myCounter, + public void resolveHeaderAndPayload(@Payload String content, @Header("myCounter") int myCounter, @Header(AmqpHeaders.CONSUMER_TAG) String tag, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { invocations.put("resolveHeaderAndPayload", true); @@ -575,9 +574,9 @@ public void invalidMessagePayloadType(Message message) { } - @SuppressWarnings("serial") static class MyBean implements Serializable { + private String name; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java new file mode 100644 index 0000000000..4186f04f05 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener; + +import java.util.Collections; +import java.util.Map; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + +/** + * @author Gary Russell + */ +public class MicrometerHolderTests { + + @Test + void multiReg() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class); + assertThatIllegalStateException().isThrownBy(() -> new MicrometerHolder(context, "", Collections.emptyMap())) + .withMessage("No micrometer registry present (or more than one and " + + "there is not exactly one marked with @Primary)"); + } + + @Test + void twoPrimaries() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config2.class); + assertThatIllegalStateException().isThrownBy(() -> new MicrometerHolder(context, "", Collections.emptyMap())) + .withMessageContaining("more than one 'primary' bean"); + } + + @Test + void primary() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config3.class); + MicrometerHolder micrometerHolder = new MicrometerHolder(ctx, "holderName", Collections.emptyMap()); + Timer.Sample sample = mock(Timer.Sample.class); + micrometerHolder.success(sample, "queue"); + micrometerHolder.failure(sample, "queue", "SomeException"); + @SuppressWarnings("unchecked") + Map meters = (Map) ReflectionTestUtils.getField(micrometerHolder, "timers"); + assertThat(meters).hasSize(2); + ctx.close(); + micrometerHolder.destroy(); + assertThat(meters).hasSize(0); + } + + static class Config1 { + + @Bean + MeterRegistry reg1() { + return new SimpleMeterRegistry(); + } + + @Bean + MeterRegistry reg2() { + return new SimpleMeterRegistry(); + } + + } + + static class Config2 { + + @Bean + @Primary + MeterRegistry reg1() { + return new SimpleMeterRegistry(); + } + + @Bean + @Primary + MeterRegistry reg2() { + return new SimpleMeterRegistry(); + } + + } + + static class Config3 { + + @Bean + @Primary + MeterRegistry reg1() { + return new SimpleMeterRegistry(); + } + + @Bean + MeterRegistry reg2() { + return new SimpleMeterRegistry(); + } + + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java new file mode 100644 index 0000000000..72e424000b --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.ChannelProxy; +import org.springframework.amqp.rabbit.connection.Connection; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Gary Russell + * @since 2.4.12 + * + */ +public class QueueDeclarationTests { + + @Test + void redeclareWhenQueue() throws IOException, InterruptedException { + AmqpAdmin admin = mock(AmqpAdmin.class); + ApplicationContext context = mock(ApplicationContext.class); + final CountDownLatch latch = new CountDownLatch(1); + SimpleMessageListenerContainer container = createContainer(admin, latch); + given(context.getBeansOfType(Queue.class, false, false)).willReturn(Map.of("foo", new Queue("test"))); + given(context.getBeansOfType(Declarables.class, false, false)).willReturn(new HashMap<>()); + container.setApplicationContext(context); + container.start(); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + verify(admin).initialize(); + container.stop(); + } + + @Test + void redeclareWhenDeclarables() throws IOException, InterruptedException { + AmqpAdmin admin = mock(AmqpAdmin.class); + ApplicationContext context = mock(ApplicationContext.class); + final CountDownLatch latch = new CountDownLatch(1); + SimpleMessageListenerContainer container = createContainer(admin, latch); + given(context.getBeansOfType(Queue.class, false, false)).willReturn(new HashMap<>()); + given(context.getBeansOfType(Declarables.class, false, false)) + .willReturn(Map.of("foo", new Declarables(List.of(new Queue("test"))))); + container.setApplicationContext(context); + container.start(); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + verify(admin).initialize(); + container.stop(); + } + + private SimpleMessageListenerContainer createContainer(AmqpAdmin admin, final CountDownLatch latch) + throws IOException { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + ChannelProxy channel = mock(ChannelProxy.class); + Channel rabbitChannel = mock(AutorecoveringChannel.class); + given(channel.getTargetChannel()).willReturn(rabbitChannel); + + given(connectionFactory.createConnection()).willReturn(connection); + given(connection.createChannel(anyBoolean())).willReturn(channel); + final AtomicBoolean isOpen = new AtomicBoolean(true); + willAnswer(i -> isOpen.get()).given(channel).isOpen(); + given(channel.queueDeclarePassive(Mockito.anyString())) + .willAnswer(invocation -> mock(AMQP.Queue.DeclareOk.class)); + given(channel.basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), + anyMap(), any(Consumer.class))).willReturn("consumerTag"); + + willAnswer(i -> { + latch.countDown(); + return null; + }).given(channel).basicQos(anyInt(), anyBoolean()); + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setQueueNames("test"); + container.setPrefetchCount(2); + container.setReceiveTimeout(10); + container.setAmqpAdmin(admin); + container.afterPropertiesSet(); + return container; + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrarTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrarTests.java index d4392ca93a..9150b4db84 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrarTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.listener; -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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,6 +23,10 @@ import org.springframework.amqp.rabbit.config.SimpleRabbitListenerEndpoint; import org.springframework.beans.factory.support.StaticListableBeanFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + /** * @author Stephane Nicoll * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistryTests.java index 5d22b5abc2..87e466e1f8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.config.RabbitListenerContainerTestFactory; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerEndpoint; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + /** * * @author Stephane Nicoll diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index 42ccdc011c..b1e52e03c6 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.awaitility.Awaitility.with; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.io.Serializable; import java.time.Duration; @@ -43,6 +31,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.Queue.DeclareOk; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -53,9 +43,11 @@ import org.springframework.amqp.AmqpIOException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AnonymousQueue; +import org.springframework.amqp.core.BatchMessageListener; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueInformation; import org.springframework.amqp.event.AmqpEvent; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.Connection; @@ -77,16 +69,26 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.log.LogMessage; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import com.rabbitmq.client.AMQP.Queue.DeclareOk; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.awaitility.Awaitility.with; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Dave Syer * @author Gunnar Hillert * @author Gary Russell * @author Artem Bilan + * @author Cao Weibo * * @since 1.3 * @@ -100,7 +102,7 @@ public class SimpleMessageListenerContainerIntegration2Tests { public static final String TEST_QUEUE_1 = "test.queue.1.SimpleMessageListenerContainerIntegration2Tests"; - private static Log logger = LogFactory.getLog(SimpleMessageListenerContainerIntegration2Tests.class); + private static final Log logger = LogFactory.getLog(SimpleMessageListenerContainerIntegration2Tests.class); private final ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -217,8 +219,8 @@ public void publishEvent(Object event) { @Override public void publishEvent(ApplicationEvent event) { - if (event instanceof AsyncConsumerStartedEvent) { - newConsumer.set(((AsyncConsumerStartedEvent) event).getConsumer()); + if (event instanceof AsyncConsumerStartedEvent asyncConsumerStartedEvent) { + newConsumer.set(asyncConsumerStartedEvent.getConsumer()); latch2.countDown(); } } @@ -317,6 +319,7 @@ public void testListenFromAnonQueue() throws Exception { container.setMessageListener(new MessageListenerAdapter(new PojoListener(latch))); container.setQueueNames(queue.getName()); container.setConcurrentConsumers(2); + container.setReceiveTimeout(10); GenericApplicationContext context = new GenericApplicationContext(); context.getBeanFactory().registerSingleton("foo", queue); context.refresh(); @@ -344,13 +347,14 @@ public void testListenFromAnonQueue() throws Exception { @Test public void testExclusive() throws Exception { Log logger = spy(TestUtils.getPropertyValue(this.template.getConnectionFactory(), "logger", Log.class)); - willReturn(true).given(logger).isInfoEnabled(); + willReturn(true).given(logger).isDebugEnabled(); new DirectFieldAccessor(this.template.getConnectionFactory()).setPropertyValue("logger", logger); CountDownLatch latch1 = new CountDownLatch(1000); SimpleMessageListenerContainer container1 = new SimpleMessageListenerContainer(template.getConnectionFactory()); container1.setMessageListener(new MessageListenerAdapter(new PojoListener(latch1))); container1.setQueueNames(queue.getName()); + container1.setReceiveTimeout(10); GenericApplicationContext context = new GenericApplicationContext(); context.getBeanFactory().registerSingleton("foo", queue); context.refresh(); @@ -362,6 +366,7 @@ public void testExclusive() throws Exception { consumeLatch1.countDown(); } }); + container1.setBeanName("container1"); container1.afterPropertiesSet(); container1.start(); assertThat(consumeLatch1.await(10, TimeUnit.SECONDS)).isTrue(); @@ -372,6 +377,7 @@ public void testExclusive() throws Exception { container2.setQueueNames(queue.getName()); container2.setApplicationContext(context); container2.setRecoveryInterval(1000); + container2.setReceiveTimeout(10); container2.setExclusive(true); // not really necessary, but likely people will make all consumers exclusive. final AtomicReference eventRef = new AtomicReference<>(); final CountDownLatch consumeLatch2 = new CountDownLatch(1); @@ -383,9 +389,10 @@ else if (event instanceof ConsumeOkEvent) { consumeLatch2.countDown(); } }); + container2.setBeanName("container2"); container2.afterPropertiesSet(); Log containerLogger = spy(TestUtils.getPropertyValue(container2, "logger", Log.class)); - willReturn(true).given(containerLogger).isWarnEnabled(); + willReturn(true).given(containerLogger).isDebugEnabled(); new DirectFieldAccessor(container2).setPropertyValue("logger", containerLogger); container2.start(); for (int i = 0; i < 1000; i++) { @@ -401,13 +408,18 @@ else if (event instanceof ConsumeOkEvent) { } assertThat(latch2.await(10, TimeUnit.SECONDS)).isTrue(); container2.stop(); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, atLeastOnce()).info(captor.capture()); - assertThat(captor.getAllValues()).anyMatch(arg -> arg.contains("exclusive")); + ArgumentCaptor connLogCaptor = ArgumentCaptor.forClass(String.class); + verify(logger, atLeastOnce()).debug(connLogCaptor.capture()); + assertThat(connLogCaptor.getAllValues()).anyMatch(arg -> arg.contains("exclusive")); assertThat(eventRef.get().getReason()).isEqualTo("Consumer raised exception, attempting restart"); assertThat(eventRef.get().isFatal()).isFalse(); assertThat(eventRef.get().getThrowable()).isInstanceOf(AmqpIOException.class); - verify(containerLogger, atLeastOnce()).warn(any()); + ArgumentCaptor contLogCaptor = ArgumentCaptor.forClass(String.class); + verify(containerLogger, atLeastOnce()).debug(contLogCaptor.capture()); + assertThat(contLogCaptor.getAllValues()).anyMatch(arg -> arg.contains("exclusive")); + ArgumentCaptor lmCaptor = ArgumentCaptor.forClass(LogMessage.class); + verify(containerLogger).debug(lmCaptor.capture()); + assertThat(lmCaptor.getAllValues()).anyMatch(arg -> arg.toString().startsWith("Restarting ")); } @Test @@ -456,6 +468,7 @@ public void basicQos(int prefetchCount, boolean global) throws IOException { container.setQueueNames(queue.getName()); container.setRecoveryInterval(500); container.setGlobalQos(true); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); @@ -507,6 +520,7 @@ public DeclareOk queueDeclarePassive(String queue) throws IOException { container.setMessageListener(new MessageListenerAdapter(new PojoListener(latch))); container.setQueueNames(queue.getName()); container.setRecoveryInterval(500); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); @@ -534,6 +548,7 @@ public void testRestartConsumerMissingQueue() throws Exception { container.setDeclarationRetries(1); container.setFailedDeclarationRetryInterval(100); container.setRetryDeclarationInterval(30000); + container.setReceiveTimeout(10); container.setApplicationEventPublisher(event -> { if (event instanceof MissingQueueEvent) { missingLatch.countDown(); @@ -641,7 +656,7 @@ public void testErrorStopsContainer() throws Exception { this.container = createContainer((m) -> { throw new Error("testError"); }, false, this.queue.getName()); - this.container.setjavaLangErrorHandler(error -> { }); + this.container.setJavaLangErrorHandler(error -> { }); final CountDownLatch latch = new CountDownLatch(1); this.container.setApplicationEventPublisher(event -> { if (event instanceof ListenerContainerConsumerFailedEvent) { @@ -681,6 +696,108 @@ public void testManualAckWithClosedChannel() throws Exception { assertThat(exc.get().getMessage()).isEqualTo("Channel closed; cannot ack/nack"); } + @Test + public void testMessageAckListenerWithSuccessfulAck() throws Exception { + final AtomicInteger calledTimes = new AtomicInteger(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + this.container = createContainer((ChannelAwareMessageListener) (m, c) -> { + }, false, this.queue.getName()); + this.container.setAcknowledgeMode(AcknowledgeMode.AUTO); + this.container.setMessageAckListener((success, deliveryTag, cause) -> { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + }); + this.container.afterPropertiesSet(); + this.container.start(); + int messageCount = 5; + for (int i = 0; i < messageCount; i++) { + this.template.convertAndSend(this.queue.getName(), "foo"); + } + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + this.container.stop(); + assertThat(calledTimes.get()).isEqualTo(messageCount); + assertThat(ackSuccess.get()).isTrue(); + assertThat(ackCause.get()).isNull(); + assertThat(ackDeliveryTag.get()).isEqualTo(messageCount); + } + + @Test + public void testMessageAckListenerWithBatchAck() throws Exception { + final AtomicInteger calledTimes = new AtomicInteger(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + this.container = createContainer((BatchMessageListener) messages -> { + }, false, this.queue.getName()); + this.container.setBatchSize(5); + this.container.setConsumerBatchEnabled(true); + this.container.setAcknowledgeMode(AcknowledgeMode.AUTO); + this.container.setMessageAckListener((success, deliveryTag, cause) -> { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + }); + this.container.afterPropertiesSet(); + this.container.start(); + int messageCount = 5; + for (int i = 0; i < messageCount; i++) { + this.template.convertAndSend(this.queue.getName(), "foo"); + } + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + this.container.stop(); + assertThat(calledTimes.get()).isEqualTo(1); + assertThat(ackSuccess.get()).isTrue(); + assertThat(ackCause.get()).isNull(); + assertThat(ackDeliveryTag.get()).isEqualTo(messageCount); + } + + @Test + void forceStop() { + CountDownLatch latch1 = new CountDownLatch(1); + this.container = createContainer((ChannelAwareMessageListener) (msg, chan) -> { + latch1.await(10, TimeUnit.SECONDS); + }, false, TEST_QUEUE); + try { + this.container.setForceStop(true); + this.template.convertAndSend(TEST_QUEUE, "one"); + this.template.convertAndSend(TEST_QUEUE, "two"); + this.template.convertAndSend(TEST_QUEUE, "three"); + this.template.convertAndSend(TEST_QUEUE, "four"); + this.template.convertAndSend(TEST_QUEUE, "five"); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(TEST_QUEUE); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(5); + }); + this.container.start(); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(TEST_QUEUE); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(0); + }); + this.container.stop(() -> { + }); + latch1.countDown(); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(TEST_QUEUE); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(4); + }); + } + finally { + this.container.stop(); + } + } + private boolean containerStoppedForAbortWithBadListener() throws InterruptedException { Log logger = spy(TestUtils.getPropertyValue(container, "logger", Log.class)); new DirectFieldAccessor(container).setPropertyValue("logger", logger); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java index ad19a5526d..104ce4fd72 100755 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -28,6 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.logging.log4j.Level; @@ -58,7 +56,9 @@ import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.awaitility.Awaitility.await; /** * @author Dave Syer @@ -314,7 +314,7 @@ private void doListenerWithExceptionTest(CountDownLatch latch, MessageListener l container.shutdown(); } if (acknowledgeMode.isTransactionAllowed()) { - assertThat(template.receiveAndConvert(queue.getName())).isNotNull(); + await().untilAsserted(() -> assertThat(template.receiveAndConvert(queue.getName())).isNotNull()); } else { assertThat(template.receiveAndConvert(queue.getName())).isNull(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java index 81faf4f84e..8e10d17204 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,13 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; - import java.util.Set; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; -import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; @@ -37,7 +33,9 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; /** * @author Gary Russell @@ -66,7 +64,6 @@ public class SimpleMessageListenerContainerLongTests { private final SingleConnectionFactory connectionFactory; - public SimpleMessageListenerContainerLongTests(ConnectionFactory connectionFactory) { this.connectionFactory = new SingleConnectionFactory(connectionFactory); } @@ -90,6 +87,7 @@ private void testChangeConsumerCountGuts(boolean transacted) throws Exception { container.setAutoStartup(false); container.setConcurrentConsumers(2); container.setChannelTransacted(transacted); + container.setReceiveTimeout(10); container.afterPropertiesSet(); assertThat(ReflectionTestUtils.getField(container, "concurrentConsumers")).isEqualTo(2); container.start(); @@ -101,7 +99,7 @@ private void testChangeConsumerCountGuts(boolean transacted) throws Exception { for (int i = 0; i < 20; i++) { template.convertAndSend(QUEUE, "foo"); } - waitForNConsumers(container, 2); // increased consumers due to work + waitForNConsumers(container, 2); // increased consumers due to work waitForNConsumers(container, 1, 20000); // should stop the extra consumer after 10 seconds idle container.setConcurrentConsumers(3); waitForNConsumers(container, 3); @@ -115,11 +113,13 @@ private void testChangeConsumerCountGuts(boolean transacted) throws Exception { } @Test - public void testAddQueuesAndStartInCycle() throws Exception { + public void testAddQueuesAndStartInCycle() { final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer( this.connectionFactory); - container.setMessageListener((MessageListener) message -> { }); + container.setMessageListener(message -> { + }); container.setConcurrentConsumers(2); + container.setReceiveTimeout(10); container.afterPropertiesSet(); RabbitAdmin admin = new RabbitAdmin(this.connectionFactory); @@ -145,6 +145,7 @@ public void testIncreaseMinAtMax() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); container.setStartConsumerMinInterval(100); container.setConsecutiveActiveTrigger(1); + container.setReceiveTimeout(10); container.setMessageListener(m -> { try { Thread.sleep(50); @@ -184,6 +185,7 @@ public void testDecreaseMinAtMax() throws Exception { container.setQueueNames(QUEUE3); container.setConcurrentConsumers(2); container.setMaxConcurrentConsumers(3); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); RabbitTemplate template = new RabbitTemplate(this.connectionFactory); @@ -212,6 +214,7 @@ public void testDecreaseMaxAtMax() throws Exception { container.setQueueNames(QUEUE4); container.setConcurrentConsumers(2); container.setMaxConcurrentConsumers(3); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); RabbitTemplate template = new RabbitTemplate(this.connectionFactory); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index 32c133ddd0..09e6a7e2a7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,26 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.awaitility.Awaitility.with; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; @@ -47,6 +27,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -56,6 +37,11 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.PossibleAuthenticationFailureException; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; @@ -64,11 +50,10 @@ import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpException; -import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -81,6 +66,7 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.aop.support.AopUtils; import org.springframework.beans.DirectFieldAccessor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; @@ -88,12 +74,24 @@ import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.util.backoff.FixedBackOff; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.PossibleAuthenticationFailureException; - +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.awaitility.Awaitility.await; +import static org.awaitility.Awaitility.with; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author David Syer @@ -101,6 +99,9 @@ * @author Gary Russell * @author Artem Bilan * @author Mohammad Hewedy + * @author Yansong Ren + * @author Tim Bourquin + * @author Jeonggi Kim */ public class SimpleMessageListenerContainerTests { @@ -112,6 +113,7 @@ public void testChannelTransactedOverriddenWhenTxManager() { container.setQueueNames("foo"); container.setChannelTransacted(false); container.setTransactionManager(new TestTransactionManager()); + container.setReceiveTimeout(10); container.afterPropertiesSet(); assertThat(TestUtils.getPropertyValue(container, "transactional", Boolean.class)).isTrue(); container.stop(); @@ -127,8 +129,9 @@ public void testInconsistentTransactionConfiguration() { container.setChannelTransacted(false); container.setAcknowledgeMode(AcknowledgeMode.NONE); container.setTransactionManager(new TestTransactionManager()); + container.setReceiveTimeout(10); assertThatIllegalStateException() - .isThrownBy(container::afterPropertiesSet); + .isThrownBy(container::afterPropertiesSet); container.stop(); singleConnectionFactory.destroy(); } @@ -141,8 +144,9 @@ public void testInconsistentAcknowledgeConfiguration() { container.setQueueNames("foo"); container.setChannelTransacted(true); container.setAcknowledgeMode(AcknowledgeMode.NONE); + container.setReceiveTimeout(10); assertThatIllegalStateException() - .isThrownBy(container::afterPropertiesSet); + .isThrownBy(container::afterPropertiesSet); container.stop(); singleConnectionFactory.destroy(); } @@ -154,6 +158,8 @@ public void testDefaultConsumerCount() { container.setMessageListener(new MessageListenerAdapter(this)); container.setQueueNames("foo"); container.setAutoStartup(false); + container.setShutdownTimeout(0); + container.setReceiveTimeout(10); container.afterPropertiesSet(); assertThat(ReflectionTestUtils.getField(container, "concurrentConsumers")).isEqualTo(1); container.stop(); @@ -204,6 +210,7 @@ public void testTxSizeAcks() throws Exception { final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setQueueNames("foo"); container.setBatchSize(2); + container.setReceiveTimeout(10); container.setMessageListener(messages::add); container.start(); BasicProperties props = new BasicProperties(); @@ -256,7 +263,10 @@ public void testTxSizeAcksWIthShortSet() throws Exception { final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setQueueNames("foobar"); container.setBatchSize(2); + container.setReceiveTimeout(10); container.setMessageListener(messages::add); + container.setShutdownTimeout(0); + container.afterPropertiesSet(); container.start(); BasicProperties props = new BasicProperties(); byte[] payload = "baz".getBytes(); @@ -303,6 +313,8 @@ public void testConsumerArgs() throws Exception { container.setMessageListener(message -> { }); container.setConsumerArguments(Collections.singletonMap("x-priority", 10)); + container.setShutdownTimeout(0); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); verify(channel).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), @@ -338,6 +350,7 @@ public void testChangeQueues() throws Exception { container.setReceiveTimeout(1); container.setMessageListener(message -> { }); + container.setShutdownTimeout(0); container.afterPropertiesSet(); container.start(); assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue(); @@ -354,6 +367,7 @@ public void testChangeQueuesSimple() { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setQueueNames("foo"); + container.setReceiveTimeout(10); List queues = TestUtils.getPropertyValue(container, "queues", List.class); assertThat(queues).hasSize(1); container.addQueueNames(new AnonymousQueue().getName(), new AnonymousQueue().getName()); @@ -384,6 +398,8 @@ public void testAddQueuesAndStartInCycle() throws Exception { final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setMessageListener(message -> { }); + container.setShutdownTimeout(0); + container.setReceiveTimeout(10); container.afterPropertiesSet(); for (int i = 0; i < 10; i++) { @@ -418,6 +434,31 @@ protected void setUpMockCancel(Channel channel, final List consumers) }).given(channel).basicCancel(anyString()); } + @Test + public void testCallbackIsRunOnStopAlsoWhenNoConsumerIsActive() throws InterruptedException { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + ReflectionTestUtils.setField(container, "active", Boolean.TRUE); + + final CountDownLatch countDownLatch = new CountDownLatch(1); + container.stop(countDownLatch::countDown); + assertThat(countDownLatch.await(100, TimeUnit.MILLISECONDS)).isTrue(); + } + + @Test + public void testCallbackIsRunOnStopAlsoWhenContainerIsStoppingForAbort() throws InterruptedException { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + ReflectionTestUtils.setField(container, "containerStoppingForAbort", new AtomicReference<>(new Thread())); + ReflectionTestUtils.setField(container, "active", Boolean.TRUE); + + final CountDownLatch countDownLatch = new CountDownLatch(1); + container.stop(countDownLatch::countDown); + assertThat(countDownLatch.await(100, TimeUnit.MILLISECONDS)).isTrue(); + } + @Test public void testWithConnectionPerListenerThread() throws Exception { com.rabbitmq.client.ConnectionFactory mockConnectionFactory = @@ -446,16 +487,17 @@ public void testWithConnectionPerListenerThread() throws Exception { container.setConcurrentConsumers(2); container.setQueueNames("foo"); container.setConsumeDelay(100); + container.setReceiveTimeout(10); container.afterPropertiesSet(); CountDownLatch latch1 = new CountDownLatch(2); CountDownLatch latch2 = new CountDownLatch(2); willAnswer(messageToConsumer(mockChannel1, container, false, latch1)) .given(mockChannel1).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), - anyMap(), any(Consumer.class)); + anyMap(), any(Consumer.class)); willAnswer(messageToConsumer(mockChannel2, container, false, latch1)) .given(mockChannel2).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), - anyMap(), any(Consumer.class)); + anyMap(), any(Consumer.class)); willAnswer(messageToConsumer(mockChannel1, container, true, latch2)).given(mockChannel1).basicCancel(anyString()); willAnswer(messageToConsumer(mockChannel2, container, true, latch2)).given(mockChannel2).basicCancel(anyString()); @@ -556,6 +598,7 @@ public void testPossibleAuthenticationFailureNotFatal() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.setQueueNames("foo"); + container.setReceiveTimeout(10); container.setPossibleAuthenticationFailureFatal(false); container.start(); @@ -566,34 +609,6 @@ public void testPossibleAuthenticationFailureNotFatal() { container.destroy(); } - @Test - public void testNullMPP() { - class Container extends SimpleMessageListenerContainer { - - @Override - public void executeListener(Channel channel, Object messageIn) { - super.executeListener(channel, messageIn); - } - - } - Container container = new Container(); - container.setMessageListener(m -> { - // NOSONAR - }); - container.setAfterReceivePostProcessors(m -> null); - container.setConnectionFactory(mock(ConnectionFactory.class)); - container.afterPropertiesSet(); - container.start(); - try { - container.executeListener(null, MessageBuilder.withBody("foo".getBytes()).build()); - fail("Expected exception"); - } - catch (ImmediateAcknowledgeAmqpException e) { - // NOSONAR - } - container.stop(); - } - @Test public void testChildClassLoader() { ClassLoader child = new URLClassLoader(new URL[0], SimpleMessageListenerContainerTests.class.getClassLoader()); @@ -632,6 +647,7 @@ class DoNothingMPP implements MessagePostProcessor { public Message postProcessMessage(Message message) throws AmqpException { return message; } + } Container container = new Container(); @@ -650,6 +666,56 @@ public Message postProcessMessage(Message message) throws AmqpException { assertThat(afterReceivePostProcessors).containsExactly(mpp2, mpp3); } + @SuppressWarnings("unchecked") + @Test + void setConcurrency() throws Exception { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + Channel channel = mock(Channel.class); + given(connectionFactory.createConnection()).willReturn(connection); + given(connection.createChannel(false)).willReturn(channel); + final AtomicReference consumer = new AtomicReference<>(); + willAnswer(invocation -> { + consumer.set(invocation.getArgument(6)); + consumer.get().handleConsumeOk("1"); + return "1"; + }).given(channel) + .basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), anyMap(), + any(Consumer.class)); + final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setQueueNames("foo", "bar"); + container.setMessageListener(mock(MessageListener.class)); + container.setConcurrency("5-10"); + container.start(); + await().until(() -> TestUtils.getPropertyValue(container, "consumers", Collection.class).size() == 5); + container.setConcurrency("10-10"); + assertThat(TestUtils.getPropertyValue(container, "consumers", Collection.class)).hasSize(10); + } + + @Test + void testWithConsumerStartWhenNotActive() { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + Channel channel = mock(Channel.class); + given(connectionFactory.createConnection()).willReturn(connection); + given(connection.createChannel(false)).willReturn(channel); + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + // overwrite task execute. shutdown container before task execute. + TestExecutor testExecutor = new TestExecutor(container); + container.setTaskExecutor(testExecutor); + container.start(); + + // then add queue for trigger container shutdown + container.addQueueNames("bar"); + + // valid the 'start' countdown is 0. lastTask is AsyncMessageProcessingConsumer + Runnable lastTask = testExecutor.getLastTask(); + CountDownLatch start = TestUtils.getPropertyValue(lastTask, "start", CountDownLatch.class); + + assertThat(start.getCount()).isEqualTo(0L); + } + private Answer messageToConsumer(final Channel mockChannel, final SimpleMessageListenerContainer container, final boolean cancel, final CountDownLatch latch) { return invocation -> { @@ -674,15 +740,15 @@ private Answer messageToConsumer(final Channel mockChannel, final Simple } - private void waitForConsumersToStop(Set consumers) throws Exception { + private void waitForConsumersToStop(Set consumers) { with().pollInterval(Duration.ofMillis(10)).atMost(Duration.ofSeconds(10)) .until(() -> consumers.stream() .map(consumer -> TestUtils.getPropertyValue(consumer, "consumer")) - .allMatch(c -> c == null)); + .allMatch(Objects::isNull)); } @SuppressWarnings("serial") - private class TestTransactionManager extends AbstractPlatformTransactionManager { + private static final class TestTransactionManager extends AbstractPlatformTransactionManager { @Override protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { @@ -703,4 +769,34 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc } + @SuppressWarnings("serial") + private static final class TestExecutor extends SimpleAsyncTaskExecutor { + + private final SimpleMessageListenerContainer simpleMessageListenerContainer; + + private int shutdownCount = 0; + + private Runnable lastTask = null; + + private TestExecutor(SimpleMessageListenerContainer simpleMessageListenerContainer) { + this.simpleMessageListenerContainer = simpleMessageListenerContainer; + } + + public Runnable getLastTask() { + return lastTask; + } + + @Override + public void execute(Runnable task) { + // skip the first execution + if (++shutdownCount > 1) { + lastTask = task; + // before execute, shutdown the container for test + this.simpleMessageListenerContainer.shutdown(); + } + super.execute(task); + } + + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java index ad3d808d7d..ad12bcdc8b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -36,6 +34,8 @@ import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.beans.DirectFieldAccessor; +import static org.assertj.core.api.Assertions.assertThat; + public final class SimpleMessageListenerWithRabbitMQ { private static Log logger = LogFactory.getLog(SimpleMessageListenerWithRabbitMQ.class); @@ -63,6 +63,7 @@ public static void main(String[] args) throws InterruptedException { container.setBatchSize(500); container.setAcknowledgeMode(AcknowledgeMode.AUTO); container.setConcurrentConsumers(20); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new SimpleAdapter(), messageConverter)); container.start(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/StopStartIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/StopStartIntegrationTests.java index 335059437f..2a33ef4cfb 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/StopStartIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/StopStartIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.fail; - import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterAll; @@ -34,6 +32,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.fail; + /** * @author Gary Russell * @author Gunnar Hillert diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/UnackedRawIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/UnackedRawIntegrationTests.java index 9236bd4622..89b4c29a3e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/UnackedRawIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/UnackedRawIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,18 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.IOException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.GetResponse; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -31,13 +36,7 @@ import org.springframework.amqp.rabbit.junit.BrokerTestUtils; import org.springframework.amqp.rabbit.support.Delivery; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.GetResponse; +import static org.assertj.core.api.Assertions.assertThat; /** * Used to verify raw Rabbit Java Client behaviour for corner cases. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java new file mode 100644 index 0000000000..ac1d11728e --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener.adapter; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Gary Russell + * @author heng zhang + * + * @since 3.0 + * + */ +@SpringJUnitConfig +@RabbitAvailable(queues = "test.batchQueue") +@DirtiesContext +public class BatchMessagingMessageListenerAdapterTests { + + @Test + void compatibleMethod() throws Exception { + Method method = getClass().getDeclaredMethod("listen", List.class); + BatchMessagingMessageListenerAdapter adapter = new BatchMessagingMessageListenerAdapter(this, method, false, + null, null); + assertThat(TestUtils.getPropertyValue(adapter, "messagingMessageConverter.inferredArgumentType")) + .isEqualTo(String.class); + Method badMethod = getClass().getDeclaredMethod("listen", String.class); + assertThatIllegalStateException().isThrownBy(() -> + new BatchMessagingMessageListenerAdapter(this, badMethod, false, null, null) + ).withMessageStartingWith("Mis-configuration"); + } + + public void listen(String in) { + } + + public void listen(List in) { + } + + + @Test + public void errorMsgConvert(@Autowired BatchMessagingMessageListenerAdapterTests.Config config, + @Autowired RabbitTemplate template) throws Exception { + + Message message = MessageBuilder.withBody(""" + { + "name" : "Tom", + "age" : 18 + } + """.getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build(); + + Message errorMessage = MessageBuilder.withBody("".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build(); + + for (int i = 0; i < config.count; i++) { + template.send("test.batchQueue", message); + template.send("test.batchQueue", errorMessage); + } + + assertThat(config.countDownLatch.await(config.count * 1000L, TimeUnit.SECONDS)).isTrue(); + } + + + + @Configuration + @EnableRabbit + public static class Config { + volatile int count = 5; + volatile CountDownLatch countDownLatch = new CountDownLatch(count); + + @RabbitListener( + queues = "test.batchQueue", + containerFactory = "batchListenerContainerFactory" + ) + public void listen(List list) { + for (Model model : list) { + countDownLatch.countDown(); + } + + } + + @Bean + ConnectionFactory cf() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean(name = "batchListenerContainerFactory") + public RabbitListenerContainerFactory rc(ConnectionFactory connectionFactory) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory); + factory.setPrefetchCount(1); + factory.setConcurrentConsumers(1); + factory.setBatchListener(true); + factory.setBatchSize(3); + factory.setConsumerBatchEnabled(true); + + Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(new ObjectMapper()); + factory.setMessageConverter(jackson2JsonMessageConverter); + + return factory; + } + + @Bean + RabbitTemplate template(ConnectionFactory cf) { + return new RabbitTemplate(cf); + } + + + } + public static class Model { + String name; + String age; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAge() { + return age; + } + + public void setAge(String age) { + this.age = age; + } + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java new file mode 100644 index 0000000000..25fcee7a00 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.listener.adapter; + +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.messaging.converter.GenericMessageConverter; +import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; +import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +/** + * @author Gary Russell + * @since 2.4.12 + * + */ +public class DelegatingInvocableHandlerTests { + + @Test + void multiNoMatch() throws Exception { + List methods = new ArrayList<>(); + Object bean = new Multi(); + Method method = Multi.class.getDeclaredMethod("listen", Integer.class); + methods.add(messageHandlerFactory().createInvocableHandlerMethod(bean, method)); + BeanExpressionResolver resolver = mock(BeanExpressionResolver.class); + BeanExpressionContext context = mock(BeanExpressionContext.class); + DelegatingInvocableHandler handler = new DelegatingInvocableHandler(methods, bean, resolver, context); + assertThatExceptionOfType(UndeclaredThrowableException.class).isThrownBy(() -> + handler.getHandlerForPayload(Long.class)) + .withCauseExactlyInstanceOf(NoSuchMethodException.class) + .withStackTraceContaining("No listener method found in"); + } + + private MessageHandlerMethodFactory messageHandlerFactory() { + DefaultMessageHandlerMethodFactory defaultFactory = new DefaultMessageHandlerMethodFactory(); + DefaultFormattingConversionService cs = new DefaultFormattingConversionService(); + defaultFactory.setConversionService(cs); + GenericMessageConverter messageConverter = new GenericMessageConverter(cs); + defaultFactory.setMessageConverter(messageConverter); + defaultFactory.afterPropertiesSet(); + return defaultFactory; + } + + public static class Multi { + + void listen(Integer in) { + } + + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java index 15b6501988..4fc1d3c433 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,38 +16,36 @@ package org.springframework.amqp.rabbit.listener.adapter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.support.SendRetryContextAccessor; -import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.aop.framework.ProxyFactory; import org.springframework.retry.RetryPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; -import com.rabbitmq.client.Channel; -import reactor.core.publisher.Mono; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Dave Syer @@ -69,8 +67,15 @@ public class MessageListenerAdapterTests { public void init() { this.messageProperties = new MessageProperties(); this.messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); - this.adapter = new MessageListenerAdapter(); - this.adapter.setMessageConverter(new SimpleMessageConverter()); + this.adapter = new MessageListenerAdapter() { + + @Override + protected void doHandleResult(InvocationResult resultArg, Message request, @Nullable Channel channel, + @Nullable Object source) { + + } + + }; } @Test @@ -79,7 +84,7 @@ class ExtendedListenerAdapter extends MessageListenerAdapter { @Override protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { - return new Object[] { extractedMessage, channel, message }; + return new Object[] {extractedMessage, channel, message}; } } @@ -133,7 +138,15 @@ public String myPojoMessageMethod(String input) { } } - this.adapter = new MessageListenerAdapter(new Delegate(), "myPojoMessageMethod"); + this.adapter = new MessageListenerAdapter(new Delegate(), "myPojoMessageMethod") { + + @Override + protected void doHandleResult(InvocationResult resultArg, Message request, @Nullable Channel channel, + @Nullable Object source) { + + } + + }; this.adapter.onMessage(new Message("foo".getBytes(), messageProperties), null); assertThat(called.get()).isTrue(); } @@ -148,7 +161,7 @@ public void testExplicitListenerMethod() throws Exception { @Test public void testMappedListenerMethod() throws Exception { - Map map = new HashMap(); + Map map = new HashMap<>(); map.put("foo", "handle"); map.put("bar", "notDefinedOnInterface"); this.adapter.setDefaultListenerMethod("anotherHandle"); @@ -188,6 +201,7 @@ public void testJdkProxyListener() throws Exception { @Test public void testReplyRetry() throws Exception { + this.adapter = new MessageListenerAdapter(); this.adapter.setDefaultListenerMethod("handle"); this.adapter.setDelegate(this.simpleService); RetryPolicy retryPolicy = new SimpleRetryPolicy(2); @@ -212,7 +226,7 @@ public void testReplyRetry() throws Exception { this.adapter.onMessage(message, channel); assertThat(this.simpleService.called).isEqualTo("handle"); assertThat(replyMessage.get()).isNotNull(); - assertThat(new String(replyMessage.get().getBody())).isEqualTo("processedfoo"); + assertThat(new String(replyMessage.get().getBody())).isEqualTo("processed foo"); assertThat(replyAddress.get()).isNotNull(); assertThat(replyAddress.get().getExchangeName()).isEqualTo("foo"); assertThat(replyAddress.get().getRoutingKey()).isEqualTo("bar"); @@ -220,13 +234,13 @@ public void testReplyRetry() throws Exception { } @Test - public void testListenableFutureReturn() throws Exception { + public void testCompletableFutureReturn() throws Exception { class Delegate { @SuppressWarnings("unused") - public ListenableFuture myPojoMessageMethod(String input) { - SettableListenableFuture future = new SettableListenableFuture<>(); - future.set("processed" + input); + public CompletableFuture myPojoMessageMethod(String input) { + CompletableFuture future = new CompletableFuture<>(); + future.complete("processed " + input); return future; } @@ -272,18 +286,18 @@ public static class SimpleService implements Service { @Override public String handle(String input) { called = "handle"; - return "processed" + input; + return "processed " + input; } @Override public String anotherHandle(String input) { called = "anotherHandle"; - return "processed" + input; + return "processed " + input; } public String notDefinedOnInterface(String input) { called = "notDefinedOnInterface"; - return "processed" + input; + return "processed " + input; } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java index a268fb295e..aa2d7ab1f2 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,6 @@ package org.springframework.amqp.rabbit.listener.adapter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -33,6 +25,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,8 +48,13 @@ import org.springframework.messaging.support.MessageBuilder; import org.springframework.util.ReflectionUtils; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Stephane Nicoll @@ -284,17 +283,10 @@ void errorHandlerAfterConversionEx() throws Exception { Channel channel = mock(Channel.class); AtomicBoolean ehCalled = new AtomicBoolean(); MessagingMessageListenerAdapter listener = getSimpleInstance("fail", - new RabbitListenerErrorHandler() { - - @Override - public Object handleError(org.springframework.amqp.core.Message amqpMessage, Message message, - ListenerExecutionFailedException exception) throws Exception { - - ehCalled.set(true); - return null; - } - - }, false, String.class); + (amqpMessage, channel1, message1, exception) -> { + ehCalled.set(true); + return null; + }, false, String.class); listener.setMessageConverter(new MessageConverter() { @Override @@ -319,17 +311,10 @@ void errorHandlerAfterConversionExWithResult() throws Exception { Channel channel = mock(Channel.class); AtomicBoolean ehCalled = new AtomicBoolean(); MessagingMessageListenerAdapter listener = getSimpleInstance("fail", - new RabbitListenerErrorHandler() { - - @Override - public Object handleError(org.springframework.amqp.core.Message amqpMessage, Message message, - ListenerExecutionFailedException exception) throws Exception { - - ehCalled.set(true); - return "foo"; - } - - }, false, String.class); + (amqpMessage, channel1, message1, exception) -> { + ehCalled.set(true); + return "foo"; + }, false, String.class); listener.setMessageConverter(new MessageConverter() { @Override @@ -502,7 +487,7 @@ public void withHeaders(Foo foo, @Headers Map headers) { } - private static class Foo { + private static final class Foo { private String foo; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java index 6f4afb5779..1e0186d313 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,6 @@ package org.springframework.amqp.rabbit.log4j2; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.net.URI; import java.util.Map; @@ -33,11 +23,16 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DefaultSaslConfig; +import com.rabbitmq.client.JDKSaslConfig; +import com.rabbitmq.client.impl.CRDemoMechanism; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LoggerContext; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.BindingBuilder; @@ -56,10 +51,15 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.DefaultSaslConfig; -import com.rabbitmq.client.JDKSaslConfig; -import com.rabbitmq.client.impl.CRDemoMechanism; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -123,6 +123,7 @@ public void test() { } @Test + @Disabled("weird - this.events.take() in appender is returning null") public void testProperties() { Logger logger = LogManager.getLogger("foo"); AmqpAppender appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", @@ -165,7 +166,12 @@ public void testProperties() { // default value assertThat(TestUtils.getPropertyValue(manager, "addMdcAsHeaders", Boolean.class)).isTrue(); - assertThat(TestUtils.getPropertyValue(appender, "events.items", Object[].class).length).isEqualTo(10); + java.util.Queue queue = TestUtils.getPropertyValue(appender, "events", java.util.Queue.class); + int i = 0; + while (queue.poll() != null) { + i++; + } + assertThat(i).isEqualTo(10); Object events = TestUtils.getPropertyValue(appender, "events"); assertThat(events.getClass()).isEqualTo(ArrayBlockingQueue.class); @@ -178,24 +184,24 @@ public void testSaslConfig() { Map.class).get("sasl1"); assertThat(RabbitUtils.stringToSaslConfig(TestUtils.getPropertyValue(appender, "manager.saslConfig", String.class), mock(ConnectionFactory.class))) - .isInstanceOf(DefaultSaslConfig.class) - .hasFieldOrPropertyWithValue("mechanism", "PLAIN"); + .isInstanceOf(DefaultSaslConfig.class) + .hasFieldOrPropertyWithValue("mechanism", "PLAIN"); appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", Map.class).get("sasl2"); assertThat(RabbitUtils.stringToSaslConfig(TestUtils.getPropertyValue(appender, "manager.saslConfig", String.class), mock(ConnectionFactory.class))) - .isInstanceOf(DefaultSaslConfig.class) - .hasFieldOrPropertyWithValue("mechanism", "EXTERNAL"); + .isInstanceOf(DefaultSaslConfig.class) + .hasFieldOrPropertyWithValue("mechanism", "EXTERNAL"); appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", Map.class).get("sasl3"); assertThat(RabbitUtils.stringToSaslConfig(TestUtils.getPropertyValue(appender, "manager.saslConfig", String.class), mock(ConnectionFactory.class))) - .isInstanceOf(JDKSaslConfig.class); + .isInstanceOf(JDKSaslConfig.class); appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", Map.class).get("sasl4"); assertThat(RabbitUtils.stringToSaslConfig(TestUtils.getPropertyValue(appender, "manager.saslConfig", String.class), mock(ConnectionFactory.class))) - .isInstanceOf(CRDemoMechanism.CRDemoSaslConfig.class); + .isInstanceOf(CRDemoMechanism.CRDemoSaslConfig.class); } @Test diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java index a9d76fa568..45b7a6ab4c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.log4j2; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.net.URI; import java.util.Map; @@ -33,6 +27,7 @@ import org.apache.logging.log4j.core.LoggerContext; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.BindingBuilder; @@ -50,6 +45,12 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + /** * @author Francesco Scipioni * @@ -115,6 +116,7 @@ public void test() { } @Test + @Disabled("weird - this.events.take() in appender is returning null") public void testProperties() { Logger logger = LogManager.getLogger("foo"); AmqpAppender appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", @@ -159,7 +161,12 @@ public void testProperties() { // default value assertThat(TestUtils.getPropertyValue(manager, "addMdcAsHeaders", Boolean.class)).isTrue(); - assertThat(TestUtils.getPropertyValue(appender, "events.items", Object[].class).length).isEqualTo(10); + java.util.Queue queue = TestUtils.getPropertyValue(appender, "events", java.util.Queue.class); + int i = 0; + while (queue.poll() != null) { + i++; + } + assertThat(i).isEqualTo(0); assertThat(TestUtils.getPropertyValue(appender, "foo")).isEqualTo("foo"); assertThat(TestUtils.getPropertyValue(appender, "bar")).isEqualTo("bar"); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderConfiguration.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderConfiguration.java index 2e46057088..17275801e4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderConfiguration.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderConfiguration.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. @@ -106,6 +106,7 @@ public SimpleMessageListenerContainer listenerContainer() { // container.setMessageListener(testListener(4)); container.setAutoStartup(false); container.setAcknowledgeMode(AcknowledgeMode.AUTO); + container.setReceiveTimeout(10); return container; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java index 8dc4affd7a..52adf6dc58 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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,15 @@ package org.springframework.amqp.rabbit.logback; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; +import ch.qos.logback.classic.Logger; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; import org.slf4j.MDC; @@ -45,7 +41,11 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import ch.qos.logback.classic.Logger; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Artem Bilan @@ -57,6 +57,7 @@ @SpringJUnitConfig(classes = AmqpAppenderConfiguration.class) @DirtiesContext @RabbitAvailable +@Disabled("Temporary") public class AmqpAppenderIntegrationTests { /* logback will automatically find lockback-test.xml */ diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java index ede1240040..ee327ffcae 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,14 @@ package org.springframework.amqp.rabbit.logback; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.net.URI; import java.net.URISyntaxException; +import com.rabbitmq.client.DefaultSaslConfig; +import com.rabbitmq.client.JDKSaslConfig; +import com.rabbitmq.client.SaslConfig; +import com.rabbitmq.client.impl.CRDemoMechanism; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -38,10 +32,16 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.DefaultSaslConfig; -import com.rabbitmq.client.JDKSaslConfig; -import com.rabbitmq.client.SaslConfig; -import com.rabbitmq.client.impl.CRDemoMechanism; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @@ -50,6 +50,7 @@ * * @since 2.0 */ +@Disabled("Temporary") public class AmqpAppenderTests { @Test diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/remoting/RemotingTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/remoting/RemotingTests.java deleted file mode 100644 index 43c940b099..0000000000 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/remoting/RemotingTests.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.amqp.rabbit.remoting; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.rabbit.junit.RabbitAvailable; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.remoting.RemoteProxyFailureException; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -/** - * @author Gary Russell - * @since 1.2 - * - */ -@SpringJUnitConfig -@DirtiesContext -@RabbitAvailable -public class RemotingTests { - - @Autowired - private ServiceInterface client; - - private static CountDownLatch latch; - - private static String receivedMessage; - - @BeforeAll - @AfterAll - public static void setupAndCleanUp() { - CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); - RabbitAdmin admin = new RabbitAdmin(cf); - admin.deleteExchange("remoting.test.exchange"); - admin.deleteQueue("remoting.test.queue"); - cf.destroy(); - } - - @Test - public void testEcho() { - String reply = client.echo("foo"); - assertThat(reply).isEqualTo("echo:foo"); - } - - @Test - public void testNoAnswer() throws Exception { - latch = new CountDownLatch(1); - client.noAnswer("foo"); - assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(receivedMessage).isEqualTo("received:foo"); - } - - @Test - public void testTimeout() { - try { - client.suspend(); - fail("Exception expected"); - } - catch (RemoteProxyFailureException e) { - assertThat(e.getMessage()).contains(" - perhaps a timeout in the template?"); - } - } - - public interface ServiceInterface { - - String echo(String message); - - void noAnswer(String message); - - void suspend(); - - } - - public static class ServiceImpl implements ServiceInterface { - - @Override - public String echo(String message) { - return "echo:" + message; - } - - @Override - public void noAnswer(String message) { - receivedMessage = "received:" + message; - latch.countDown(); - } - - @Override - public void suspend() { - try { - Thread.sleep(3000); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - - } -} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java index eef04a3a71..6c6a63e890 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,6 @@ package org.springframework.amqp.rabbit.retry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.atMost; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -61,6 +51,16 @@ import org.springframework.retry.support.RetrySynchronizationManager; import org.springframework.retry.support.RetryTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @author Arnaud Cogoluègnes @@ -96,6 +96,7 @@ public void testWithNoId() throws Exception { container.setStatefulRetryFatalWithNullMessageId(false); container.setMessageListener(new MessageListenerAdapter(new POJO())); container.setQueueNames("retry.test.queue"); + container.setReceiveTimeout(10); StatefulRetryOperationsInterceptorFactoryBean fb = new StatefulRetryOperationsInterceptorFactoryBean(); @@ -134,6 +135,7 @@ public void testWithId() throws Exception { ConnectionFactory connectionFactory = ctx.getBean(ConnectionFactory.class); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setPrefetchCount(1); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new POJO())); container.setQueueNames("retry.test.queue"); @@ -197,6 +199,7 @@ public void testWithIdAndSuccess() throws Exception { } }); container.setQueueNames("retry.test.queue"); + container.setReceiveTimeout(10); StatefulRetryOperationsInterceptorFactoryBean fb = new StatefulRetryOperationsInterceptorFactoryBean(); @@ -221,7 +224,7 @@ public void testWithIdAndSuccess() throws Exception { try { assertThat(cdl.await(30, TimeUnit.SECONDS)).isTrue(); Map map = (Map) new DirectFieldAccessor(cache).getPropertyValue("map"); - await().until(() -> map.size() == 0); + await().until(map::isEmpty); ArgumentCaptor putCaptor = ArgumentCaptor.forClass(Object.class); ArgumentCaptor getCaptor = ArgumentCaptor.forClass(Object.class); ArgumentCaptor removeCaptor = ArgumentCaptor.forClass(Object.class); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java index 14f53fb581..6f8fa8a1e0 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or 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,10 @@ package org.springframework.amqp.rabbit.retry; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.PrintWriter; import java.io.StringWriter; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; @@ -31,9 +30,16 @@ import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Gary Russell + * @author Artem Bilan + * * @since 2.0.5 * */ @@ -49,9 +55,12 @@ class RepublishMessageRecovererIntegrationTests { private int maxHeaderSize; @Test + @Disabled("Need to figure out the failure on CI") void testBigHeader() { CachingConnectionFactory ccf = new CachingConnectionFactory( RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + ApplicationContext applicationContext = mock(); + ccf.setApplicationContext(applicationContext); RabbitTemplate template = new RabbitTemplate(ccf); this.maxHeaderSize = RabbitUtils.getMaxFrame(template.getConnectionFactory()) - RepublishMessageRecoverer.DEFAULT_FRAME_MAX_HEADROOM; @@ -69,7 +78,8 @@ void testBigHeader() { "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."; assertThat(trace).contains(truncatedMessage); assertThat((String) received.getMessageProperties().getHeader(RepublishMessageRecoverer.X_EXCEPTION_MESSAGE)) - .isEqualTo(truncatedMessage); + .isEqualTo(truncatedMessage); + ccf.onApplicationEvent(new ContextClosedEvent(applicationContext)); ccf.destroy(); } @@ -77,6 +87,8 @@ void testBigHeader() { void testSmallException() { CachingConnectionFactory ccf = new CachingConnectionFactory( RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + ApplicationContext applicationContext = mock(); + ccf.setApplicationContext(applicationContext); RabbitTemplate template = new RabbitTemplate(ccf); this.maxHeaderSize = RabbitUtils.getMaxFrame(template.getConnectionFactory()) - RepublishMessageRecoverer.DEFAULT_FRAME_MAX_HEADROOM; @@ -91,6 +103,7 @@ void testSmallException() { String trace = received.getMessageProperties().getHeaders() .get(RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE).toString(); assertThat(trace).isEqualTo(getStackTraceAsString(cause)); + ccf.onApplicationEvent(new ContextClosedEvent(applicationContext)); ccf.destroy(); } @@ -99,6 +112,8 @@ void testBigMessageSmallTrace() { CachingConnectionFactory ccf = new CachingConnectionFactory( RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); RabbitTemplate template = new RabbitTemplate(ccf); + ApplicationContext applicationContext = mock(); + ccf.setApplicationContext(applicationContext); this.maxHeaderSize = RabbitUtils.getMaxFrame(template.getConnectionFactory()) - RepublishMessageRecoverer.DEFAULT_FRAME_MAX_HEADROOM; assertThat(this.maxHeaderSize).isGreaterThan(0); @@ -117,6 +132,7 @@ void testBigMessageSmallTrace() { .getHeader(RepublishMessageRecoverer.X_EXCEPTION_MESSAGE).toString(); assertThat(trace.length() + exceptionMessage.length()).isEqualTo(this.maxHeaderSize); assertThat(exceptionMessage).endsWith("..."); + ccf.onApplicationEvent(new ContextClosedEvent(applicationContext)); ccf.destroy(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTest.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java similarity index 81% rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTest.java rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java index 4f5e912a26..04bdf93009 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTest.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or 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.amqp.rabbit.retry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; - import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.Map; @@ -33,6 +30,10 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; /** * @author James Carr @@ -42,7 +43,7 @@ * @since 1.3 */ @ExtendWith(MockitoExtension.class) -public class RepublishMessageRecovererTest { +public class RepublishMessageRecovererTests { private final Message message = new Message("".getBytes(), new MessageProperties()); @@ -151,4 +152,29 @@ void setDeliveryModeIfNull() { assertThat(this.message.getMessageProperties().getDeliveryMode()).isEqualTo(MessageDeliveryMode.NON_PERSISTENT); } + @Test + void dynamicExRk() { + this.recoverer = new RepublishMessageRecoverer(this.amqpTemplate, + new SpelExpressionParser().parseExpression("messageProperties.headers.get('errorExchange')"), + new SpelExpressionParser().parseExpression("messageProperties.headers.get('errorRK')")); + this.message.getMessageProperties().setHeader("errorExchange", "ex"); + this.message.getMessageProperties().setHeader("errorRK", "rk"); + + this.recoverer.recover(this.message, this.cause); + + verify(this.amqpTemplate).send("ex", "rk", this.message); + } + + @Test + void dynamicRk() { + this.recoverer = new RepublishMessageRecoverer(this.amqpTemplate, null, + new SpelExpressionParser().parseExpression("messageProperties.headers.get('errorRK')")); + this.message.getMessageProperties().setHeader("errorExchange", "ex"); + this.message.getMessageProperties().setHeader("errorRK", "rk"); + + this.recoverer.recover(this.message, this.cause); + + verify(this.amqpTemplate).send("rk", this.message); + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java index dae6bf2ed4..023f4972ef 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 the original author or 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,6 @@ package org.springframework.amqp.rabbit.retry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.AmqpMessageReturnedException; @@ -35,6 +31,12 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; /** * @author Gary Russell @@ -103,15 +105,24 @@ void testCorrelatedWithNack() { RabbitTemplate template = new RabbitTemplate(ccf); RabbitAdmin admin = new RabbitAdmin(ccf); Queue queue = QueueBuilder.durable(QUEUE + ".nack") - .maxLength(1) + .maxLength(1L) .overflow(Overflow.rejectPublish) .build(); + admin.deleteQueue(queue.getName()); admin.declareQueue(queue); - RepublishMessageRecovererWithConfirms recoverer = new RepublishMessageRecovererWithConfirms(template, "", - queue.getName(), ConfirmType.CORRELATED); - recoverer.recover(MessageBuilder.withBody("foo".getBytes()).build(), new RuntimeException()); - assertThatExceptionOfType(AmqpNackReceivedException.class).isThrownBy(() -> - recoverer.recover(MessageBuilder.withBody("foo".getBytes()).build(), new RuntimeException())); + + RepublishMessageRecovererWithConfirms recoverer = new RepublishMessageRecovererWithConfirms(template, + new LiteralExpression(""), + new SpelExpressionParser().parseExpression("messageProperties.headers[queueName]"), + ConfirmType.CORRELATED); + + Message message = MessageBuilder.withBody("foo".getBytes()).setHeader("queueName", queue.getName()).build(); + + recoverer.recover(message, new RuntimeException()); + + assertThatExceptionOfType(AmqpNackReceivedException.class) + .isThrownBy(() -> recoverer.recover(message, new RuntimeException())); + admin.deleteQueue(queue.getName()); ccf.destroy(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/ActiveObjectCounterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/ActiveObjectCounterTests.java index fc779506eb..488a4b5a27 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/ActiveObjectCounterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/ActiveObjectCounterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.support; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java index 9a76f1de66..dd2cc1eab7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.support; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.DataInputStream; import java.io.UnsupportedEncodingException; import java.util.Arrays; @@ -25,20 +23,22 @@ import java.util.List; import java.util.Map; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.LongString; +import com.rabbitmq.client.impl.LongStringHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.LongString; -import com.rabbitmq.client.impl.LongStringHelper; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Soeren Unruh * @author Gary Russell + * @author Johan Kaving * @since 1.3 */ public class DefaultMessagePropertiesConverterTests { @@ -104,6 +104,21 @@ public void testToMessagePropertiesLongStringInMap() { assertThat(((Map) messageProperties.getHeaders().get("map")).get("longString")).as("LongString nested in Map not converted to String").isEqualTo(longStringString); } + @Test + public void testToMessagePropertiesXDeathCount() { + Map headers = new HashMap(); + + headers.put("x-death", List.of(Map.of("count", Integer.valueOf(2)))); + + BasicProperties source = new BasicProperties.Builder() + .headers(headers) + .build(); + + MessageProperties messageProperties = messagePropertiesConverter.toMessageProperties(source, envelope, "UTF-8"); + + assertThat(messageProperties.getRetryCount()).isEqualTo(2); + } + @Test public void testLongLongString() { Map headers = new HashMap(); @@ -201,6 +216,17 @@ public void testClassHeader() { assertThat(basic.getHeaders().get("aClass")).isEqualTo(getClass().getName()); } + @Test + public void testRetryCount() { + MessageProperties props = new MessageProperties(); + props.incrementRetryCount(); + BasicProperties basic = new DefaultMessagePropertiesConverter().fromMessageProperties(props, "UTF8"); + assertThat(basic.getHeaders().get(MessageProperties.RETRY_COUNT)).isEqualTo(1L); + props.incrementRetryCount(); + basic = new DefaultMessagePropertiesConverter().fromMessageProperties(props, "UTF8"); + assertThat(basic.getHeaders().get(MessageProperties.RETRY_COUNT)).isEqualTo(2L); + } + private static class Foo { Foo() { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java new file mode 100644 index 0000000000..828d31892b --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java @@ -0,0 +1,189 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Span.Kind; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.simple.SpanAssert; +import io.micrometer.tracing.test.simple.SpansAssert; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +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; + +/** + * @author Artem Bilan + * @author Gary Russell + * + * @since 3.0 + */ +@RabbitAvailable(queues = { "int.observation.testQ1", "int.observation.testQ2" }) +public class ObservationIntegrationTests extends SampleTestRunner { + + @Override + public SampleTestRunnerConsumer yourCode() { + // template -> listener -> template -> listener + return (bb, meterRegistry) -> { + ObservationRegistry observationRegistry = getObservationRegistry(); + try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) { + applicationContext.registerBean(ObservationRegistry.class, () -> observationRegistry); + applicationContext.register(Config.class); + applicationContext.refresh(); + applicationContext.getBean(RabbitTemplate.class).convertAndSend("int.observation.testQ1", "test"); + assertThat(applicationContext.getBean(Listener.class).latch1.await(10, TimeUnit.SECONDS)).isTrue(); + } + + List finishedSpans = bb.getFinishedSpans(); + SpansAssert.assertThat(finishedSpans) + .haveSameTraceId() + .hasSize(4); + List producerSpans = finishedSpans.stream() + .filter(span -> span.getKind().equals(Kind.PRODUCER)) + .toList(); + List consumerSpans = finishedSpans.stream() + .filter(span -> span.getKind().equals(Kind.CONSUMER)) + .toList(); + SpanAssert.assertThat(producerSpans.get(0)) + .hasTag("spring.rabbit.template.name", "template") + .hasTag("messaging.destination.name", "") + .hasTag("messaging.rabbitmq.destination.routing_key", "int.observation.testQ1"); + SpanAssert.assertThat(producerSpans.get(0)) + .hasRemoteServiceNameEqualTo("RabbitMQ"); + SpanAssert.assertThat(producerSpans.get(1)) + .hasTag("spring.rabbit.template.name", "template") + .hasTag("messaging.destination.name", "") + .hasTag("messaging.rabbitmq.destination.routing_key", "int.observation.testQ2"); + SpanAssert.assertThat(consumerSpans.get(0)) + .hasTagWithKey("spring.rabbit.listener.id") + .hasTag("messaging.destination.name", "int.observation.testQ1") + .hasTag("messaging.rabbitmq.message.delivery_tag", "1"); + SpanAssert.assertThat(consumerSpans.get(0)) + .hasRemoteServiceNameEqualTo("RabbitMQ"); + assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.listener.id")).isIn("obs1", "obs2"); + SpanAssert.assertThat(consumerSpans.get(1)) + .hasTagWithKey("spring.rabbit.listener.id"); + assertThat(consumerSpans.get(1).getTags().get("spring.rabbit.listener.id")).isIn("obs1", "obs2"); + SpanAssert.assertThat(consumerSpans.get(1)) + .hasTagWithKey("spring.rabbit.listener.id") + .hasTag("messaging.destination.name", "int.observation.testQ2") + .hasTag("messaging.rabbitmq.message.delivery_tag", "1"); + assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.listener.id")) + .isNotEqualTo(consumerSpans.get(1).getTags().get("spring.rabbit.listener.id")); + + MeterRegistryAssert.assertThat(getMeterRegistry()) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of( + KeyValue.of("spring.rabbit.template.name", "template"), + KeyValue.of("messaging.destination.name", ""), + KeyValue.of("messaging.rabbitmq.destination.routing_key", "int.observation.testQ1") + ) + ) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of( + KeyValue.of("spring.rabbit.template.name", "template"), + KeyValue.of("messaging.destination.name", ""), + KeyValue.of("messaging.rabbitmq.destination.routing_key", "int.observation.testQ2") + ) + ) + .hasTimerWithNameAndTags("spring.rabbit.listener", + KeyValues.of( + KeyValue.of("spring.rabbit.listener.id", "obs1"), + KeyValue.of("messaging.destination.name", "int.observation.testQ1") + ) + ) + .hasTimerWithNameAndTags("spring.rabbit.listener", + KeyValues.of( + KeyValue.of("spring.rabbit.listener.id", "obs2"), + KeyValue.of("messaging.destination.name", "int.observation.testQ2") + ) + ); + }; + } + + @Configuration + @EnableRabbit + public static class Config { + + @Bean + CachingConnectionFactory ccf() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + RabbitTemplate template(CachingConnectionFactory ccf) { + RabbitTemplate template = new RabbitTemplate(ccf); + template.setObservationEnabled(true); + return template; + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory ccf) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(ccf); + factory.setContainerCustomizer(container -> container.setObservationEnabled(true)); + return factory; + } + + @Bean + Listener listener(RabbitTemplate template) { + return new Listener(template); + } + + } + + public static class Listener { + + private final RabbitTemplate template; + + final CountDownLatch latch1 = new CountDownLatch(1); + + public Listener(RabbitTemplate template) { + this.template = template; + } + + @RabbitListener(id = "obs1", queues = "int.observation.testQ1") + void listen1(Message in) { + this.template.convertAndSend("int.observation.testQ2", in); + } + + @RabbitListener(id = "obs2", queues = "int.observation.testQ2") + void listen2(Message in) { + this.latch1.countDown(); + } + + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java new file mode 100644 index 0000000000..d7e6e196fe --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java @@ -0,0 +1,292 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.DefaultTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; +import io.micrometer.tracing.propagation.Propagator; +import io.micrometer.tracing.test.simple.SimpleSpan; +import io.micrometer.tracing.test.simple.SimpleTracer; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation.DefaultRabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservation.DefaultRabbitTemplateObservationConvention; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * @author Gary Russell + * @author Ngoc Nhan + * @author Artem Bilan + * + * @since 3.0 + * + */ +@SpringJUnitConfig +@RabbitAvailable(queues = { "observation.testQ1", "observation.testQ2" }) +@DirtiesContext +public class ObservationTests { + + @Test + void endToEnd(@Autowired Listener listener, @Autowired RabbitTemplate template, + @Autowired SimpleTracer tracer, @Autowired RabbitListenerEndpointRegistry rler, + @Autowired MeterRegistry meterRegistry) + throws InterruptedException { + + template.convertAndSend("observation.testQ1", "test"); + assertThat(listener.latch1.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(listener.message) + .extracting(msg -> msg.getMessageProperties().getHeaders()) + .hasFieldOrPropertyWithValue("foo", "some foo value") + .hasFieldOrPropertyWithValue("bar", "some bar value"); + Deque spans = tracer.getSpans(); + await().until(() -> spans.size() == 4); + SimpleSpan span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); + await().until(() -> spans.peekFirst().getTags().size() == 5); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf( + Map.of("spring.rabbit.listener.id", "obs1", "foo", "some foo value", "bar", "some bar value")); + assertThat(span.getName()).isEqualTo("observation.testQ1 receive"); + await().until(() -> spans.peekFirst().getTags().size() == 3); + span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getName()).isEqualTo("/observation.testQ2 send"); + await().until(() -> spans.peekFirst().getTags().size() == 5); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf( + Map.of("spring.rabbit.listener.id", "obs2", "foo", "some foo value", "bar", "some bar value")); + assertThat(span.getName()).isEqualTo("observation.testQ2 receive"); + template.setObservationConvention(new DefaultRabbitTemplateObservationConvention() { + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { + return super.getLowCardinalityKeyValues(context).and("foo", "bar") + .and("messaging.destination.name", context.getExchange()) + .and("messaging.rabbitmq.destination.routing_key", context.getRoutingKey()); + } + + }); + ((AbstractMessageListenerContainer) rler.getListenerContainer("obs1")).setObservationConvention( + new DefaultRabbitListenerObservationConvention() { + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { + return super.getLowCardinalityKeyValues(context).and("baz", "qux"); + } + + }); + rler.getListenerContainer("obs1").stop(); + rler.getListenerContainer("obs1").start(); + template.convertAndSend("observation.testQ1", "test"); + assertThat(listener.latch2.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(listener.message) + .extracting(msg -> msg.getMessageProperties().getHeaders()) + .hasFieldOrPropertyWithValue("foo", "some foo value") + .hasFieldOrPropertyWithValue("bar", "some bar value"); + assertThat(spans).hasSize(4); + span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getTags()).containsEntry("foo", "bar"); + assertThat(span.getTags()).containsEntry("messaging.destination.name", ""); + assertThat(span.getTags()).containsEntry("messaging.rabbitmq.destination.routing_key", "observation.testQ1"); + assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); + await().until(() -> spans.peekFirst().getTags().size() == 6); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf(Map.of("spring.rabbit.listener.id", "obs1", "foo", "some foo value", "bar", + "some bar value", "baz", "qux")); + assertThat(span.getName()).isEqualTo("observation.testQ1 receive"); + await().until(() -> spans.peekFirst().getTags().size() == 4); + span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getTags()).containsEntry("foo", "bar"); + assertThat(span.getTags()).containsEntry("messaging.destination.name", ""); + assertThat(span.getTags()).containsEntry("messaging.rabbitmq.destination.routing_key", "observation.testQ2"); + assertThat(span.getName()).isEqualTo("/observation.testQ2 send"); + await().until(() -> spans.peekFirst().getTags().size() == 5); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf( + Map.of("spring.rabbit.listener.id", "obs2", "foo", "some foo value", "bar", "some bar value")); + assertThat(span.getTags()).doesNotContainEntry("baz", "qux"); + assertThat(span.getName()).isEqualTo("observation.testQ2 receive"); + MeterRegistryAssert.assertThat(meterRegistry) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template")) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template", "foo", "bar")) + .hasTimerWithNameAndTags("spring.rabbit.listener", KeyValues.of("spring.rabbit.listener.id", "obs1")) + .hasTimerWithNameAndTags("spring.rabbit.listener", + KeyValues.of("spring.rabbit.listener.id", "obs1", "baz", "qux")) + .hasTimerWithNameAndTags("spring.rabbit.listener", KeyValues.of("spring.rabbit.listener.id", "obs2")); + } + + @Configuration + @EnableRabbit + public static class Config { + + @Bean + CachingConnectionFactory ccf() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + RabbitTemplate template(CachingConnectionFactory ccf) { + RabbitTemplate template = new RabbitTemplate(ccf); + template.setObservationEnabled(true); + return template; + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory ccf) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(ccf); + factory.setContainerCustomizer(container -> container.setObservationEnabled(true)); + return factory; + } + + @Bean + SimpleTracer simpleTracer() { + return new SimpleTracer(); + } + + @Bean + MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + + @Bean + ObservationRegistry observationRegistry(Tracer tracer, Propagator propagator, MeterRegistry meterRegistry) { + TestObservationRegistry observationRegistry = TestObservationRegistry.create(); + observationRegistry.observationConfig().observationHandler( + // Composite will pick the first matching handler + new ObservationHandler.FirstMatchingCompositeObservationHandler( + // This is responsible for creating a child span on the sender side + new PropagatingSenderTracingObservationHandler<>(tracer, propagator), + // This is responsible for creating a span on the receiver side + new PropagatingReceiverTracingObservationHandler<>(tracer, propagator), + // This is responsible for creating a default span + new DefaultTracingObservationHandler(tracer))) + .observationHandler(new DefaultMeterObservationHandler(meterRegistry)); + return observationRegistry; + } + + @Bean + Propagator propagator(Tracer tracer) { + return new Propagator() { + + // List of headers required for tracing propagation + @Override + public List fields() { + return Arrays.asList("foo", "bar"); + } + + // This is called on the producer side when the message is being sent + // Normally we would pass information from tracing context - for tests we don't need to + @Override + public void inject(TraceContext context, @Nullable C carrier, Setter setter) { + setter.set(carrier, "foo", "some foo value"); + setter.set(carrier, "bar", "some bar value"); + } + + // This is called on the consumer side when the message is consumed + // Normally we would use tools like Extractor from tracing but for tests we are just manually creating a span + @Override + public Span.Builder extract(C carrier, Getter getter) { + String foo = getter.get(carrier, "foo"); + String bar = getter.get(carrier, "bar"); + return tracer.spanBuilder().tag("foo", foo).tag("bar", bar); + } + }; + } + + @Bean + Listener listener(RabbitTemplate template) { + return new Listener(template); + } + + } + + public static class Listener { + + private final RabbitTemplate template; + + final CountDownLatch latch1 = new CountDownLatch(1); + + final CountDownLatch latch2 = new CountDownLatch(2); + + volatile Message message; + + public Listener(RabbitTemplate template) { + this.template = template; + } + + @RabbitListener(id = "obs1", queues = "observation.testQ1") + void listen1(Message in) { + this.template.send("observation.testQ2", in); + } + + @RabbitListener(id = "obs2", queues = "observation.testQ2") + void listen2(Message in) { + this.message = in; + this.latch1.countDown(); + this.latch2.countDown(); + } + + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitExceptionTranslatorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitExceptionTranslatorTests.java index ea67881884..1472a44dcd 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitExceptionTranslatorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitExceptionTranslatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,12 @@ package org.springframework.amqp.rabbit.transaction; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.ConnectException; +import com.rabbitmq.client.PossibleAuthenticationFailureException; +import com.rabbitmq.client.ShutdownSignalException; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpAuthenticationException; @@ -32,8 +32,7 @@ import org.springframework.amqp.UncategorizedAmqpException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; -import com.rabbitmq.client.PossibleAuthenticationFailureException; -import com.rabbitmq.client.ShutdownSignalException; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Sergey Shcherbakov @@ -56,6 +55,7 @@ public void testConvertRabbitAccessException() { assertThat(RabbitExceptionTranslator.convertRabbitAccessException(new UnsupportedEncodingException())).isInstanceOf(AmqpUnsupportedEncodingException.class); assertThat(RabbitExceptionTranslator.convertRabbitAccessException(new Exception() { + private static final long serialVersionUID = 1L; })).isInstanceOf(UncategorizedAmqpException.class); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManagerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManagerIntegrationTests.java index 10ebbc501a..c9cd983542 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManagerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManagerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2019 the original author or authors. + * Copyright 2011-2025 the original author or 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.amqp.rabbit.transaction; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,6 +26,9 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * @author David Syer * @author Gunnar Hillert @@ -135,9 +135,11 @@ public void testSendInTransactionWithRollback() throws Exception { @SuppressWarnings("serial") private class PlannedException extends RuntimeException { + PlannedException() { super("Planned"); } + } } diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index 9960d52cad..e06b53a03c 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-2025 the original author 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,15 +17,22 @@ package org.springframework.amqp.rabbit.annotation import assertk.assertThat +import assertk.assertions.containsOnly import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotNull import assertk.assertions.isTrue import org.junit.jupiter.api.Test +import org.springframework.amqp.core.AcknowledgeMode +import org.springframework.amqp.core.Message import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory import org.springframework.amqp.rabbit.core.RabbitTemplate import org.springframework.amqp.rabbit.junit.RabbitAvailable import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler +import org.springframework.amqp.utils.test.TestUtils import org.springframework.aop.framework.ProxyFactory import org.springframework.beans.BeansException import org.springframework.beans.factory.annotation.Autowired @@ -37,6 +44,7 @@ import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.junit.jupiter.SpringJUnitConfig import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean /** * Kotlin Annotated listener tests. @@ -48,7 +56,7 @@ import java.util.concurrent.TimeUnit * */ @SpringJUnitConfig -@RabbitAvailable(queues = ["kotlinQueue", "kotlinQueue1", "kotlinReplyQueue"]) +@RabbitAvailable(queues = ["kotlinQueue", "kotlinBatchQueue", "kotlinQueue1", "kotlinReplyQueue"]) @DirtiesContext class EnableRabbitKotlinTests { @@ -56,49 +64,82 @@ class EnableRabbitKotlinTests { private lateinit var config: Config @Test - fun `send and wait for consume` () { + fun `send and wait for consume`(@Autowired registry: RabbitListenerEndpointRegistry) { val template = RabbitTemplate(this.config.cf()) - template.convertAndSend("kotlinQueue", "test") - assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue(); + template.setReplyTimeout(10_000) + val result = template.convertSendAndReceive("kotlinQueue", "test") + assertThat(result).isEqualTo("TEST") + val listener = registry.getListenerContainer("single")?.messageListener + assertThat(listener).isNotNull() + listener?.let { nonNullableListener -> + assertThat(TestUtils.getPropertyValue(nonNullableListener, "messagingMessageConverter.inferredArgumentType") + .toString()) + .isEqualTo("class java.lang.String") + } + } + + @Test + fun `listen for batch`() { + val template = RabbitTemplate(this.config.cf()) + template.convertAndSend("kotlinBatchQueue", "test1") + template.convertAndSend("kotlinBatchQueue", "test2") + assertThat(this.config.batchReceived.await(10, TimeUnit.SECONDS)).isTrue() + assertThat(this.config.batch[0]).isInstanceOf(Message::class.java) + assertThat(this.config.batch.map { m -> String(m.body) }).containsOnly("test1", "test2") } @Test - fun `send and wait for consume with EH` () { + fun `send and wait for consume with EH`() { val template = RabbitTemplate(this.config.cf()) template.convertAndSend("kotlinQueue1", "test") - assertThat(this.config.ehLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.config.ehLatch.await(10, TimeUnit.SECONDS)).isTrue() val reply = template.receiveAndConvert("kotlinReplyQueue", 10_000) - assertThat(reply).isEqualTo("error processed"); + assertThat(reply).isEqualTo("error processed") } @Configuration @EnableRabbit class Config { - val latch = CountDownLatch(1) + @RabbitListener(id = "single", queues = ["kotlinQueue"]) + suspend fun handle(@Suppress("UNUSED_PARAMETER") data: String) : String? { + return data.uppercase() + } + + val batchReceived = CountDownLatch(1) - @RabbitListener(queues = ["kotlinQueue"]) - fun handle(@Suppress("UNUSED_PARAMETER") data: String) { - this.latch.countDown() + lateinit var batch: List + + @RabbitListener(id = "batch", queues = ["kotlinBatchQueue"], + containerFactory = "batchRabbitListenerContainerFactory") + suspend fun receiveBatch(messages: List) { + batch = messages + batchReceived.countDown() } @Bean - fun rabbitListenerContainerFactory(cf: CachingConnectionFactory): SimpleRabbitListenerContainerFactory { - val factory = SimpleRabbitListenerContainerFactory() - factory.setConnectionFactory(cf) - return factory - } + fun rabbitListenerContainerFactory(cf: CachingConnectionFactory) = + SimpleRabbitListenerContainerFactory().also { + it.setAcknowledgeMode(AcknowledgeMode.MANUAL) + it.setReceiveTimeout(10) + it.setConnectionFactory(cf) + } @Bean - fun cf(): CachingConnectionFactory { - return CachingConnectionFactory( - RabbitAvailableCondition.getBrokerRunning().connectionFactory) - } + fun batchRabbitListenerContainerFactory(cf: CachingConnectionFactory) = + SimpleRabbitListenerContainerFactory().also { + it.setAcknowledgeMode(AcknowledgeMode.MANUAL) + it.setConsumerBatchEnabled(true) + it.setDeBatchingEnabled(true) + it.setBatchSize(3) + it.setConnectionFactory(cf) + } @Bean - fun multi(): Multi { - return Multi() - } + fun cf() = CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().connectionFactory) + + @Bean + fun multi() = Multi() @Bean fun proxyListenerPostProcessor(): BeanPostProcessor? { @@ -118,14 +159,14 @@ class EnableRabbitKotlinTests { val ehLatch = CountDownLatch(1) @Bean - fun eh() = RabbitListenerErrorHandler { _, _, _ -> + fun eh() = RabbitListenerErrorHandler { _, _, _, _ -> this.ehLatch.countDown() "error processed" } } - @RabbitListener(queues = ["kotlinQueue1"], errorHandler = "#{eh}") + @RabbitListener(id = "multi", queues = ["kotlinQueue1"], errorHandler = "#{eh}") @SendTo("kotlinReplyQueue") open class Multi { diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-config.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-config.xml index 237a528ca1..a4993af914 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-config.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-config.xml @@ -14,7 +14,7 @@ - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-configurable-config.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-configurable-config.xml index 21cc8555a3..c4d5336307 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-configurable-config.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-configurable-config.xml @@ -17,7 +17,7 @@ - + - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/rabbit-listener.properties b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/rabbit-listener.properties index 991af5e15b..61aa68f57f 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/rabbit-listener.properties +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/rabbit-listener.properties @@ -4,3 +4,5 @@ rabbit.listener.queue=queue1 rabbit.listener.priority=34 rabbit.listener.responseRoutingKey=routing-123 rabbit.listener.admin=rabbitAdmin + +foo.and.bar=foo, bar diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests-context.xml index 4ae8d756dc..b471046269 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests-context.xml @@ -15,7 +15,8 @@ connection-name-strategy="connectionNameStrategy"/> - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ExchangeParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ExchangeParserTests-context.xml index 616f63239f..a2004ce3c4 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ExchangeParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ExchangeParserTests-context.xml @@ -51,7 +51,8 @@ - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests-context.xml index 35c7c46591..725bdb42c8 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests-context.xml @@ -11,6 +11,7 @@ 5 1 false + foo, bar @@ -28,4 +29,13 @@ + + + + + + + + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserPlaceholderTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserPlaceholderTests-context.xml index f90c955380..41a47bfe93 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserPlaceholderTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserPlaceholderTests-context.xml @@ -87,7 +87,8 @@ - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserTests-context.xml index 676c05ef0e..4923ec7cfc 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserTests-context.xml @@ -75,7 +75,8 @@ - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml index c031ae70c7..26b70d6de7 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml @@ -34,7 +34,7 @@ + mandatory="true" returns-callback="rcb" confirm-callback="ccb"/> - + - + - + diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/SpecialException.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactory.java similarity index 57% rename from spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/SpecialException.java rename to spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactory.java index e8a9de7102..7ccf89ebd2 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/SpecialException.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2025 the original author 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,21 +14,19 @@ * limitations under the License. */ -package org.springframework.amqp.remoting.testservice; +package org.springframework.amqp.rabbitmq.client; + +import com.rabbitmq.client.amqp.Connection; /** - * @author David Bilge - * @since 1.2 + * The contract for RabbitMQ AMQP 1.0 {@link Connection} management. + * + * @author Artem Bilan + * + * @since 4.0 */ -public class SpecialException extends RuntimeException { - private static final long serialVersionUID = 7254934411128057730L; - - public SpecialException(String message, Throwable cause) { - super(message, cause); - } +public interface AmqpConnectionFactory { - public SpecialException(String message) { - super(message); - } + Connection getConnection(); } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java new file mode 100644 index 0000000000..ada9e14a9f --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java @@ -0,0 +1,565 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client; + +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; + +import com.rabbitmq.client.amqp.AmqpException; +import com.rabbitmq.client.amqp.Management; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.Declarable; +import org.springframework.amqp.core.DeclarableCustomizer; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueInformation; +import org.springframework.amqp.rabbit.core.DeclarationExceptionEvent; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.log.LogAccessor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.util.Assert; + +/** + * The {@link AmqpAdmin} implementation for RabbitMQ AMQP 1.0 client. + * + * @author Artem Bilan + * + * @since 4.0 + */ +@ManagedResource(description = "Admin Tasks") +public class RabbitAmqpAdmin + implements AmqpAdmin, ApplicationContextAware, ApplicationEventPublisherAware, BeanNameAware, SmartLifecycle { + + private static final LogAccessor LOG = new LogAccessor(RabbitAmqpAdmin.class); + + public static final String QUEUE_TYPE = "QUEUE_TYPE"; + + private final AmqpConnectionFactory connectionFactory; + + private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + + private boolean ignoreDeclarationExceptions; + + private @Nullable ApplicationContext applicationContext; + + private @Nullable ApplicationEventPublisher applicationEventPublisher; + + @SuppressWarnings("NullAway.Init") + private String beanName; + + private boolean explicitDeclarationsOnly; + + private boolean autoStartup = true; + + private volatile @Nullable DeclarationExceptionEvent lastDeclarationExceptionEvent; + + private volatile boolean running = false; + + public RabbitAmqpAdmin(AmqpConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + public void setIgnoreDeclarationExceptions(boolean ignoreDeclarationExceptions) { + this.ignoreDeclarationExceptions = ignoreDeclarationExceptions; + } + + /** + * Set a task executor to use for async operations. Currently only used + * with {@link #purgeQueue(String, boolean)}. + * @param taskExecutor the executor to use. + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + Assert.notNull(taskExecutor, "'taskExecutor' cannot be null"); + this.taskExecutor = taskExecutor; + } + + /** + * Set to true to only declare {@link Declarable} beans that are explicitly configured + * to be declared by this admin. + * @param explicitDeclarationsOnly true to ignore beans with no admin declaration + * configuration. + */ + public void setExplicitDeclarationsOnly(boolean explicitDeclarationsOnly) { + this.explicitDeclarationsOnly = explicitDeclarationsOnly; + } + + /** + * @return the last {@link DeclarationExceptionEvent} that was detected in this admin. + */ + public @Nullable DeclarationExceptionEvent getLastDeclarationExceptionEvent() { + return this.lastDeclarationExceptionEvent; + } + + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + + @Override + public void start() { + if (!this.running) { + initialize(); + this.running = true; + } + } + + @Override + public void stop() { + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + /** + * Declares all the exchanges, queues and bindings in the enclosing application context, if any. It should be safe + * (but unnecessary) to call this method more than once. + */ + @Override + public void initialize() { + declareDeclarableBeans(); + } + + /** + * Process bean declarables. + */ + private void declareDeclarableBeans() { + if (this.applicationContext == null) { + LOG.debug("no ApplicationContext has been set, cannot auto-declare Exchanges, Queues, and Bindings"); + return; + } + + LOG.debug("Initializing declarations"); + Collection contextExchanges = new LinkedList<>( + this.applicationContext.getBeansOfType(Exchange.class).values()); + Collection contextQueues = new LinkedList<>( + this.applicationContext.getBeansOfType(Queue.class).values()); + Collection contextBindings = new LinkedList<>( + this.applicationContext.getBeansOfType(Binding.class).values()); + Collection customizers = + this.applicationContext.getBeansOfType(DeclarableCustomizer.class).values(); + + processDeclarables(contextExchanges, contextQueues, contextBindings, + this.applicationContext.getBeansOfType(Declarables.class, false, true).values()); + + final Collection exchanges = filterDeclarables(contextExchanges, customizers); + final Collection queues = filterDeclarables(contextQueues, customizers); + final Collection bindings = filterDeclarables(contextBindings, customizers); + + for (Exchange exchange : exchanges) { + if ((!exchange.isDurable() || exchange.isAutoDelete())) { + LOG.info(() -> "Auto-declaring a non-durable or auto-delete Exchange (" + + exchange.getName() + + ") durable:" + exchange.isDurable() + ", auto-delete:" + exchange.isAutoDelete() + ". " + + "It will be deleted by the broker if it shuts down, and can be redeclared by closing and " + + "reopening the connection."); + } + } + + for (Queue queue : queues) { + if ((!queue.isDurable() || queue.isAutoDelete() || queue.isExclusive())) { + LOG.info(() -> "Auto-declaring a non-durable, auto-delete, or exclusive Queue (" + + queue.getName() + + ") durable:" + queue.isDurable() + ", auto-delete:" + queue.isAutoDelete() + ", exclusive:" + + queue.isExclusive() + ". " + + "It will be redeclared if the broker stops and is restarted while the connection factory is " + + "alive, but all messages will be lost."); + } + } + + if (exchanges.isEmpty() && queues.isEmpty() && bindings.isEmpty()) { + LOG.debug("Nothing to declare"); + return; + } + + try (Management management = getManagement()) { + exchanges.forEach((exchange) -> doDeclareExchange(management, exchange)); + queues.forEach((queue) -> doDeclareQueue(management, queue)); + bindings.forEach((binding) -> doDeclareBinding(management, binding)); + } + + LOG.debug("Declarations finished"); + } + + /** + * Remove any instances that should not be declared by this admin. + * @param declarables the collection of {@link Declarable}s. + * @param customizers a collection if {@link DeclarableCustomizer} beans. + * @param the declarable type. + * @return a new collection containing {@link Declarable}s that should be declared by this + * admin. + */ + @SuppressWarnings({"unchecked", "NullAway"}) // Dataflow analysis limitation + private Collection filterDeclarables(Collection declarables, + Collection customizers) { + + return declarables.stream() + .filter(dec -> dec.shouldDeclare() && declarableByMe(dec)) + .map(dec -> { + if (customizers.isEmpty()) { + return dec; + } + AtomicReference ref = new AtomicReference<>(dec); + customizers.forEach(cust -> ref.set((T) cust.apply(ref.get()))); + return ref.get(); + }) + .toList(); + } + + private boolean declarableByMe(T dec) { + return (dec.getDeclaringAdmins().isEmpty() && !this.explicitDeclarationsOnly) // NOSONAR boolean complexity + || dec.getDeclaringAdmins().contains(this) + || dec.getDeclaringAdmins().contains(this.beanName); + } + + @Override + public void declareExchange(Exchange exchange) { + try (Management management = getManagement()) { + doDeclareExchange(management, exchange); + } + } + + private void doDeclareExchange(Management management, Exchange exchange) { + Management.ExchangeSpecification exchangeSpecification = + management.exchange(exchange.getName()) + .type(exchange.isDelayed() ? RabbitAdmin.DELAYED_MESSAGE_EXCHANGE : exchange.getType()) +// .internal(exchange.isInternal()) + .arguments(exchange.getArguments()) + .autoDelete(exchange.isAutoDelete()); + + if (exchange.isDelayed()) { + exchangeSpecification.argument("x-delayed-type", exchange.getType()); + } + try { + exchangeSpecification.declare(); + } + catch (AmqpException ex) { + logOrRethrowDeclarationException(exchange, "exchange", ex); + } + } + + @Override + @ManagedOperation(description = "Delete an exchange from the broker") + public boolean deleteExchange(String exchangeName) { + if (isDeletingDefaultExchange(exchangeName)) { + return false; + } + + try (Management management = getManagement()) { + management.exchangeDelete(exchangeName); + } + return true; + } + + @Override + public @Nullable Queue declareQueue() { + try (Management management = getManagement()) { + return doDeclareQueue(management); + } + } + + private @Nullable Queue doDeclareQueue(Management management) { + try { + Management.QueueInfo queueInfo = + management.queue() + .autoDelete(true) + .exclusive(true) + .classic() + .queue() + .declare(); + + return new Queue(queueInfo.name(), false, true, true); + } + catch (AmqpException ex) { + logOrRethrowDeclarationException(null, "queue", ex); + } + return null; + } + + @Override + public @Nullable String declareQueue(Queue queue) { + try (Management management = getManagement()) { + return doDeclareQueue(management, queue); + } + } + + private @Nullable String doDeclareQueue(Management management, Queue queue) { + Management.QueueSpecification queueSpecification = + management.queue(queue.getName()) + .autoDelete(queue.isAutoDelete()) + .exclusive(queue.isExclusive()) + .arguments(queue.getArguments()) + .classic() + .queue(); + + try { + String actualName = queueSpecification.declare().name(); + queue.setActualName(actualName); + return actualName; + } + catch (AmqpException ex) { + logOrRethrowDeclarationException(queue, "queue", ex); + } + return null; + } + + @Override + @ManagedOperation(description = "Delete a queue from the broker") + public boolean deleteQueue(String queueName) { + deleteQueue(queueName, false, false); + return true; + } + + @Override + @ManagedOperation(description = + "Delete a queue from the broker if unused and empty (when corresponding arguments are true") + public void deleteQueue(String queueName, boolean unused, boolean empty) { + try (Management management = getManagement()) { + Management.QueueInfo queueInfo = management.queueInfo(queueName); + if ((!unused || queueInfo.consumerCount() == 0) + && (!empty || queueInfo.messageCount() == 0)) { + + management.queueDelete(queueName); + } + } + } + + @Override + @ManagedOperation(description = "Purge a queue and optionally don't wait for the purge to occur") + public void purgeQueue(String queueName, boolean noWait) { + if (noWait) { + this.taskExecutor.execute(() -> purgeQueue(queueName)); + } + else { + purgeQueue(queueName); + } + } + + @Override + @ManagedOperation(description = "Purge a queue and return the number of messages purged") + public int purgeQueue(String queueName) { + try (Management management = getManagement()) { + return (int) management.queuePurge(queueName).messageCount(); + } + } + + @Override + public void declareBinding(Binding binding) { + try (Management management = getManagement()) { + doDeclareBinding(management, binding); + } + } + + private void doDeclareBinding(Management management, Binding binding) { + try { + Management.BindingSpecification bindingSpecification = + management.binding() + .sourceExchange(binding.getExchange()) + .key(binding.getRoutingKey()) + .arguments(binding.getArguments()); + if (binding.isDestinationQueue()) { + bindingSpecification.destinationQueue(binding.getDestination()); + } + else { + bindingSpecification.destinationExchange(binding.getDestination()); + } + bindingSpecification.bind(); + } + catch (AmqpException ex) { + logOrRethrowDeclarationException(binding, "binding", ex); + } + } + + @Override + public void removeBinding(Binding binding) { + if (binding.isDestinationQueue() && isRemovingImplicitQueueBinding(binding)) { + return; + } + + try (Management management = getManagement()) { + Management.UnbindSpecification unbindSpecification = + management.unbind() + .sourceExchange(binding.getExchange()) + .key(binding.getRoutingKey()) + .arguments(binding.getArguments()); + + if (binding.isDestinationQueue()) { + unbindSpecification.destinationQueue(binding.getDestination()); + } + else { + unbindSpecification.destinationExchange(binding.getDestination()); + } + unbindSpecification.unbind(); + } + } + + /** + * Returns 4 properties {@link RabbitAdmin#QUEUE_NAME}, {@link RabbitAdmin#QUEUE_MESSAGE_COUNT}, + * {@link RabbitAdmin#QUEUE_CONSUMER_COUNT}, {@link #QUEUE_TYPE}, or null if the queue doesn't exist. + */ + @Override + @ManagedOperation(description = "Get queue name, message count and consumer count") + public @Nullable Properties getQueueProperties(final String queueName) { + QueueInformation queueInfo = getQueueInfo(queueName); + if (queueInfo != null) { + Properties props = new Properties(); + props.put(RabbitAdmin.QUEUE_NAME, queueInfo.getName()); + props.put(RabbitAdmin.QUEUE_MESSAGE_COUNT, queueInfo.getMessageCount()); + props.put(RabbitAdmin.QUEUE_CONSUMER_COUNT, queueInfo.getConsumerCount()); + props.put(QUEUE_TYPE, queueInfo.getType()); + return props; + } + else { + return null; + } + } + + @Override + public @Nullable QueueInformation getQueueInfo(String queueName) { + try (Management management = getManagement()) { + Management.QueueInfo queueInfo = management.queueInfo(queueName); + QueueInformation queueInformation = + new QueueInformation(queueInfo.name(), queueInfo.messageCount(), queueInfo.consumerCount()); + queueInformation.setType(queueInfo.type().name().toLowerCase()); + return queueInformation; + } + } + + private Management getManagement() { + return this.connectionFactory.getConnection().management(); + } + + private void logOrRethrowDeclarationException(@Nullable Declarable element, + String elementType, T t) throws T { + + publishDeclarationExceptionEvent(element, t); + if (this.ignoreDeclarationExceptions || (element != null && element.isIgnoreDeclarationExceptions())) { + if (LOG.isDebugEnabled()) { + LOG.debug(t, "Failed to declare " + elementType + + ": " + (element == null ? "broker-generated" : element) + + ", continuing..."); + } + else if (LOG.isWarnEnabled()) { + Throwable cause = t; + if (t instanceof IOException && t.getCause() != null) { + cause = t.getCause(); + } + LOG.warn("Failed to declare " + elementType + + ": " + (element == null ? "broker-generated" : element) + + ", continuing... " + cause); + } + } + else { + throw t; + } + } + + private void publishDeclarationExceptionEvent(@Nullable Declarable element, Throwable ex) { + DeclarationExceptionEvent event = new DeclarationExceptionEvent(this, element, ex); + this.lastDeclarationExceptionEvent = event; + if (this.applicationEventPublisher != null) { + this.applicationEventPublisher.publishEvent(event); + } + } + + private static boolean isDeletingDefaultExchange(String exchangeName) { + if (isDefaultExchange(exchangeName)) { + LOG.warn("Default exchange cannot be deleted."); + return true; + } + return false; + } + + private static boolean isDefaultExchange(@Nullable String exchangeName) { + return exchangeName == null || RabbitAdmin.DEFAULT_EXCHANGE_NAME.equals(exchangeName); + } + + private static boolean isRemovingImplicitQueueBinding(Binding binding) { + if (isImplicitQueueBinding(binding)) { + LOG.warn("Cannot remove implicit default exchange binding to queue."); + return true; + } + return false; + } + + private static boolean isImplicitQueueBinding(Binding binding) { + return isDefaultExchange(binding.getExchange()) && + Objects.equals(binding.getDestination(), binding.getRoutingKey()); + } + + private static void processDeclarables(Collection contextExchanges, Collection contextQueues, + Collection contextBindings, Collection declarables) { + + declarables.forEach(d -> { + d.getDeclarables().forEach(declarable -> { + if (declarable instanceof Exchange exch) { + contextExchanges.add(exch); + } + else if (declarable instanceof Queue queue) { + contextQueues.add(queue); + } + else if (declarable instanceof Binding binding) { + contextBindings.add(binding); + } + }); + }); + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java new file mode 100644 index 0000000000..27b2d0cf21 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java @@ -0,0 +1,674 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Publisher; +import com.rabbitmq.client.amqp.Resource; +import com.rabbitmq.client.amqp.RpcClient; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpIllegalStateException; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.amqp.core.AsyncAmqpTemplate; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.core.ReceiveAndReplyCallback; +import org.springframework.amqp.core.ReceiveAndReplyMessageCallback; +import org.springframework.amqp.rabbit.core.AmqpNackReceivedException; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.amqp.support.converter.SmartMessageConverter; +import org.springframework.amqp.utils.JavaUtils; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.log.LogAccessor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * The {@link AmqpTemplate} for RabbitMQ AMQP 1.0 protocol support. + * A Spring-friendly wrapper around {@link Environment#connectionBuilder()}; + * + * @author Artem Bilan + * + * @since 4.0 + */ +public class RabbitAmqpTemplate implements AsyncAmqpTemplate, DisposableBean { + + private static final LogAccessor LOG = new LogAccessor(RabbitAmqpAdmin.class); + + private final AmqpConnectionFactory connectionFactory; + + private final Lock instanceLock = new ReentrantLock(); + + private @Nullable Object publisher; + + private MessageConverter messageConverter = new SimpleMessageConverter(); + + private @Nullable String defaultExchange; + + private @Nullable String defaultRoutingKey; + + private @Nullable String defaultQueue; + + private @Nullable String defaultReceiveQueue; + + private @Nullable String defaultReplyToQueue; + + private Resource.StateListener @Nullable [] stateListeners; + + private Duration publishTimeout = Duration.ofSeconds(60); + + private Duration completionTimeout = Duration.ofSeconds(60); + + public RabbitAmqpTemplate(AmqpConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + public void setListeners(Resource.StateListener... listeners) { + this.stateListeners = listeners; + } + + public void setPublishTimeout(Duration timeout) { + this.publishTimeout = timeout; + } + + /** + * Set a duration for {@link CompletableFuture#orTimeout(long, TimeUnit)} on returns. + * There is no {@link CompletableFuture} API like {@code onTimeout()} requested + * from the {@link CompletableFuture#get(long, TimeUnit)}, + * but used in operations AMQP resources have to be closed eventually independently + * of the {@link CompletableFuture} fulfilment. + * Defaults to 1 minute. + * @param completionTimeout duration for future completions. + */ + public void setCompletionTimeout(Duration completionTimeout) { + this.completionTimeout = completionTimeout; + } + + /** + * Set a default exchange for publishing. + * Cannot be real default AMQP exchange. + * The {@link #setQueue(String)} is recommended instead. + * Mutually exclusive with {@link #setQueue(String)}. + * @param exchange the default exchange + */ + public void setExchange(String exchange) { + this.defaultExchange = exchange; + } + + /** + * Set a default routing key. + * Mutually exclusive with {@link #setQueue(String)}. + * @param routingKey the default routing key. + */ + public void setRoutingKey(String routingKey) { + this.defaultRoutingKey = routingKey; + } + + /** + * Set default queue for publishing. + * Mutually exclusive with {@link #setExchange(String)} and {@link #setRoutingKey(String)}. + * @param queue the default queue. + */ + public void setQueue(String queue) { + this.defaultQueue = queue; + } + + /** + * The name of the default queue to receive messages from when none is specified explicitly. + * @param queue the default queue name to use for receive operation. + */ + public void setReceiveQueue(String queue) { + this.defaultReceiveQueue = queue; + } + + /** + * The name of the default queue to receive replies from when none is specified explicitly. + * @param queue the default queue name to use for send-n-receive operation. + */ + public void setReplyToQueue(String queue) { + this.defaultReplyToQueue = queue; + } + + /** + * Set a converter for {@link #convertAndSend(Object)} operations. + * @param messageConverter the converter. + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + private String getRequiredQueue() throws IllegalStateException { + String name = this.defaultReceiveQueue; + Assert.state(name != null, "No 'queue' specified. Check configuration of this 'RabbitAmqpTemplate'."); + return name; + } + + private Publisher getPublisher() { + Object publisherToReturn = this.publisher; + if (publisherToReturn == null) { + this.instanceLock.lock(); + try { + publisherToReturn = this.publisher; + if (publisherToReturn == null) { + publisherToReturn = + this.connectionFactory.getConnection() + .publisherBuilder() + .listeners(this.stateListeners) + .publishTimeout(this.publishTimeout) + .build(); + this.publisher = publisherToReturn; + } + } + finally { + this.instanceLock.unlock(); + } + } + return (Publisher) publisherToReturn; + } + + @Override + public void destroy() { + Object publisherToClose = this.publisher; + if (publisherToClose != null) { + ((Publisher) publisherToClose).close(); + this.publisher = null; + } + } + + /** + * Publish a message to the default exchange and routing key (if any) (or queue) configured on this template. + * @param message to publish + * @return the {@link CompletableFuture} as an async result of the message publication. + */ + @Override + public CompletableFuture send(Message message) { + Assert.state(this.defaultExchange != null || this.defaultQueue != null, + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); + return doSend(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message); + } + + /** + * Publish the message to the provided queue. + * @param queue to publish + * @param message to publish + * @return the {@link CompletableFuture} as an async result of the message publication. + */ + @Override + public CompletableFuture send(String queue, Message message) { + return doSend(null, null, queue, message); + } + + @Override + public CompletableFuture send(String exchange, @Nullable String routingKey, Message message) { + return doSend(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, message); + } + + private CompletableFuture doSend(@Nullable String exchange, @Nullable String routingKey, + @Nullable String queue, Message message) { + + com.rabbitmq.client.amqp.Message amqpMessage = + toAmqpMessage(exchange, routingKey, queue, message, getPublisher()::message); + + CompletableFuture publishResult = new CompletableFuture<>(); + + getPublisher().publish(amqpMessage, + (context) -> { + switch (context.status()) { + case ACCEPTED -> publishResult.complete(true); + case REJECTED, RELEASED -> publishResult.completeExceptionally( + new AmqpNackReceivedException("The message was rejected", message)); + } + }); + + return publishResult; + } + + /** + * Publish a message from converted body to the default exchange + * and routing key (if any) (or queue) configured on this template. + * @param message to publish + * @return the {@link CompletableFuture} as an async result of the message publication. + */ + @Override + public CompletableFuture convertAndSend(Object message) { + Assert.state(this.defaultExchange != null || this.defaultQueue != null, + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); + return doConvertAndSend(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message, null); + } + + @Override + public CompletableFuture convertAndSend(String queue, Object message) { + return doConvertAndSend(null, null, queue, message, null); + } + + @Override + public CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message) { + return doConvertAndSend(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, message, null); + } + + @Override + public CompletableFuture convertAndSend(Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + return doConvertAndSend(null, null, null, message, messagePostProcessor); + } + + @Override + public CompletableFuture convertAndSend(String queue, Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + return doConvertAndSend(null, null, queue, message, messagePostProcessor); + } + + @Override + public CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + return doConvertAndSend(exchange, routingKey, null, message, messagePostProcessor); + } + + private CompletableFuture doConvertAndSend(@Nullable String exchange, @Nullable String routingKey, + @Nullable String queue, Object data, @Nullable MessagePostProcessor messagePostProcessor) { + + Message message = convertToMessageIfNecessary(data); + if (messagePostProcessor != null) { + message = messagePostProcessor.postProcessMessage(message); + } + return doSend(exchange, routingKey, queue, message); + } + + @Override + public CompletableFuture receive() { + return receive(getRequiredQueue()); + } + + /** + * Request a head message from the provided queue. + * A returned {@link CompletableFuture} timeouts after {@link #setCompletionTimeout(Duration)}. + * @param queueName the queue to consume message from. + * @return the future with a received message. + * @see #setCompletionTimeout(Duration) + */ + @SuppressWarnings("try") + @Override + public CompletableFuture receive(String queueName) { + CompletableFuture messageFuture = new CompletableFuture<>(); + + AtomicBoolean messageReceived = new AtomicBoolean(); + + Consumer consumer = + this.connectionFactory.getConnection() + .consumerBuilder() + .queue(queueName) + .initialCredits(1) + .priority(10) + .messageHandler((context, message) -> { + if (messageReceived.compareAndSet(false, true)) { + context.accept(); + messageFuture.complete(RabbitAmqpUtils.fromAmqpMessage(message, null)); + } + }) + .build(); + + return messageFuture + .orTimeout(this.completionTimeout.toMillis(), TimeUnit.MILLISECONDS) + .whenComplete((message, exception) -> consumer.close()); + } + + @Override + public CompletableFuture receiveAndConvert() { + return receiveAndConvert((ParameterizedTypeReference) null); + } + + @Override + public CompletableFuture receiveAndConvert(String queueName) { + return receiveAndConvert(queueName, null); + } + + /** + * Receive a message from {@link #setReceiveQueue(String)} and convert its body + * to the expected type. + * The {@link #setMessageConverter(MessageConverter)} must be an implementation of {@link SmartMessageConverter}. + * @param type the type to covert received result. + * @return the CompletableFuture with a result. + */ + @Override + public CompletableFuture receiveAndConvert(@Nullable ParameterizedTypeReference type) { + return receiveAndConvert(getRequiredQueue(), type); + } + + /** + * Receive a message from {@link #setReceiveQueue(String)} and convert its body + * to the expected type. + * The {@link #setMessageConverter(MessageConverter)} must be an implementation of {@link SmartMessageConverter}. + * @param queueName the queue to consume message from. + * @param type the type to covert received result. + * @return the CompletableFuture with a result. + */ + @Override + public CompletableFuture receiveAndConvert(String queueName, @Nullable ParameterizedTypeReference type) { + return receive(queueName) + .thenApply((message) -> convertReply(message, type)); + } + + @SuppressWarnings("unchecked") + private T convertReply(Message message, @Nullable ParameterizedTypeReference type) { + if (type != null) { + return (T) getRequiredSmartMessageConverter().fromMessage(message, type); + } + else { + return (T) this.messageConverter.fromMessage(message); + } + } + + private SmartMessageConverter getRequiredSmartMessageConverter() throws IllegalStateException { + Assert.state(this.messageConverter instanceof SmartMessageConverter, + "template's message converter must be a SmartMessageConverter"); + return (SmartMessageConverter) this.messageConverter; + } + + @Override + public CompletableFuture receiveAndReply(ReceiveAndReplyCallback callback) { + return receiveAndReply(getRequiredQueue(), callback); + } + + @Override + @SuppressWarnings("try") + public CompletableFuture receiveAndReply(String queueName, ReceiveAndReplyCallback callback) { + CompletableFuture rpcFuture = new CompletableFuture<>(); + + Consumer.MessageHandler consumerHandler = + (context, message) -> { + Message requestMessage = RabbitAmqpUtils.fromAmqpMessage(message, null); + try { + Object messageId = message.messageId(); + Assert.notNull(messageId, + "The 'message-id' property has to be set on request. Used for reply correlation."); + String replyTo = message.replyTo(); + Assert.hasText(replyTo, + "The 'reply-to' property has to be set on request. Used for reply publishing."); + Message reply = handleRequestAndProduceReply(requestMessage, callback); + if (reply == null) { + LOG.info(() -> "No reply for request: " + requestMessage); + context.accept(); + rpcFuture.complete(false); + } + else { + com.rabbitmq.client.amqp.Message replyMessage = getPublisher().message(); + RabbitAmqpUtils.toAmqpMessage(reply, replyMessage); + replyMessage.correlationId(messageId); + replyMessage.to(replyTo); + getPublisher().publish(replyMessage, (ctx) -> { + }); + context.accept(); + rpcFuture.complete(true); + } + } + catch (Exception ex) { + context.discard(); + rpcFuture.completeExceptionally( + new AmqpIllegalStateException("Failed to process RPC request: " + requestMessage, ex)); + } + }; + + Consumer consumer = + this.connectionFactory.getConnection() + .consumerBuilder() + .queue(queueName) + .initialCredits(1) + .priority(10) + .messageHandler(consumerHandler) + .build(); + + return rpcFuture + .orTimeout(this.completionTimeout.toMillis(), TimeUnit.MILLISECONDS) + .whenComplete((message, exception) -> consumer.close()); + } + + @SuppressWarnings("unchecked") + private @Nullable Message handleRequestAndProduceReply(Message requestMessage, + ReceiveAndReplyCallback callback) { + + Object receive = requestMessage; + if (!(ReceiveAndReplyMessageCallback.class.isAssignableFrom(callback.getClass()))) { + receive = this.messageConverter.fromMessage(requestMessage); + } + + S reply; + try { + reply = callback.handle((R) receive); + } + catch (ClassCastException ex) { + StackTraceElement[] trace = ex.getStackTrace(); + if (trace[0].getMethodName().equals("handle") + && Objects.equals(trace[1].getFileName(), "RabbitAmqpTemplate.java")) { + + throw new IllegalArgumentException("ReceiveAndReplyCallback '" + callback + + "' can't handle received object '" + receive + "'", ex); + } + else { + throw ex; + } + } + + if (reply != null) { + return convertToMessageIfNecessary(reply); + } + return null; + } + + private Message convertToMessageIfNecessary(Object data) { + if (data instanceof Message msg) { + return msg; + } + else { + return this.messageConverter.toMessage(data, new MessageProperties()); + } + } + + @Override + public CompletableFuture sendAndReceive(Message message) { + Assert.state(this.defaultExchange != null || this.defaultQueue != null, + "For send-n-receive with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); + return doSendAndReceive(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message); + } + + @Override + public CompletableFuture sendAndReceive(String exchange, @Nullable String routingKey, Message message) { + return doSendAndReceive(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, message); + } + + @Override + public CompletableFuture sendAndReceive(String queue, Message message) { + return doSendAndReceive(null, null, queue, message); + } + + @SuppressWarnings("try") + private CompletableFuture doSendAndReceive(@Nullable String exchange, @Nullable String routingKey, + @Nullable String queue, Message message) { + + MessageProperties messageProperties = message.getMessageProperties(); + String messageId = messageProperties.getMessageId(); + String correlationId = messageProperties.getCorrelationId(); + String replyTo = messageProperties.getReplyTo(); + + // HTTP over AMQP 1.0 extension specification, 5.1: + // To associate a response with a request, the correlation-id value of the response properties + // MUST be set to the message-id value of the request properties. + // So, this supplier will override request message-id, respectively. + // Otherwise, the RpcClient generates correlation-id internally. + Supplier correlationIdSupplier = null; + if (StringUtils.hasText(correlationId)) { + correlationIdSupplier = () -> correlationId; + } + else if (StringUtils.hasText(messageId)) { + correlationIdSupplier = () -> messageId; + } + + // The default reply-to queue, or the one supplied in the message. + // Otherwise, the RpcClient generates one as exclusive and auto-delete. + String replyToQueue = this.defaultReplyToQueue; + if (StringUtils.hasText(replyTo)) { + replyToQueue = replyTo; + } + + RpcClient rpcClient = + this.connectionFactory.getConnection() + .rpcClientBuilder() + .requestTimeout(this.publishTimeout) + .correlationIdSupplier(correlationIdSupplier) + .replyToQueue(replyToQueue) + .build(); + + com.rabbitmq.client.amqp.Message amqpMessage = + toAmqpMessage(exchange, routingKey, queue, message, rpcClient::message); + + return rpcClient.publish(amqpMessage) + .thenApply((reply) -> RabbitAmqpUtils.fromAmqpMessage(reply, null)) + .orTimeout(this.completionTimeout.toMillis(), TimeUnit.MILLISECONDS) + .whenComplete((replyMessage, exception) -> rpcClient.close()); + } + + @Override + public CompletableFuture convertSendAndReceive(Object object) { + return convertSendAndReceiveAsType(object, null, null); + } + + @Override + public CompletableFuture convertSendAndReceive(String queue, Object object) { + return convertSendAndReceiveAsType(queue, object, null, null); + } + + @Override + public CompletableFuture convertSendAndReceive(String exchange, @Nullable String routingKey, Object object) { + return convertSendAndReceiveAsType(exchange, routingKey, object, null, null); + } + + @Override + public CompletableFuture convertSendAndReceive(Object object, MessagePostProcessor messagePostProcessor) { + return convertSendAndReceiveAsType(object, messagePostProcessor, null); + } + + @Override + public CompletableFuture convertSendAndReceive(String queue, Object object, + MessagePostProcessor messagePostProcessor) { + + return convertSendAndReceiveAsType(queue, object, messagePostProcessor, null); + } + + @Override + public CompletableFuture convertSendAndReceive(String exchange, @Nullable String routingKey, + Object object, @Nullable MessagePostProcessor messagePostProcessor) { + + return convertSendAndReceiveAsType(exchange, routingKey, object, messagePostProcessor, null); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(Object object, + ParameterizedTypeReference responseType) { + + return convertSendAndReceiveAsType(object, null, responseType); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(String queue, Object object, + ParameterizedTypeReference responseType) { + + return convertSendAndReceiveAsType(queue, object, null, responseType); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(String exchange, @Nullable String routingKey, + Object object, ParameterizedTypeReference responseType) { + + return convertSendAndReceiveAsType(exchange, routingKey, object, null, responseType); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(Object object, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + + Assert.state(this.defaultExchange != null || this.defaultQueue != null, + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); + + return doConvertSendAndReceive(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, object, + messagePostProcessor, responseType); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(String queue, Object object, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + + return doConvertSendAndReceive(null, null, queue, object, messagePostProcessor, responseType); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(String exchange, @Nullable String routingKey, + Object object, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable ParameterizedTypeReference responseType) { + + return doConvertSendAndReceive(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, + object, messagePostProcessor, responseType); + } + + private CompletableFuture doConvertSendAndReceive(@Nullable String exchange, @Nullable String routingKey, + @Nullable String queue, Object data, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable ParameterizedTypeReference responseType) { + + Message message = convertToMessageIfNecessary(data); + if (messagePostProcessor != null) { + message = messagePostProcessor.postProcessMessage(message); + } + return doSendAndReceive(exchange, routingKey, queue, message) + .thenApply((reply) -> convertReply(reply, responseType)); + } + + private static com.rabbitmq.client.amqp.Message toAmqpMessage(@Nullable String exchange, + @Nullable String routingKey, @Nullable String queue, Message message, + Supplier amqpMessageSupplier) { + + com.rabbitmq.client.amqp.Message.MessageAddressBuilder address = + amqpMessageSupplier.get() + .toAddress(); + + JavaUtils.INSTANCE + .acceptIfNotNull(exchange, address::exchange) + .acceptIfNotNull(routingKey, address::key) + .acceptIfNotNull(queue, address::queue); + + com.rabbitmq.client.amqp.Message amqpMessage = address.message(); + + RabbitAmqpUtils.toAmqpMessage(message, amqpMessage); + + return amqpMessage; + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java new file mode 100644 index 0000000000..f19a865947 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java @@ -0,0 +1,151 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client; + +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +import com.rabbitmq.client.amqp.Consumer; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.utils.JavaUtils; + +/** + * The utilities for RabbitMQ AMQP 1.0 protocol API. + */ +public final class RabbitAmqpUtils { + + /** + * Convert {@link com.rabbitmq.client.amqp.Message} into {@link Message}. + * @param amqpMessage the {@link com.rabbitmq.client.amqp.Message} convert from. + * @param context the {@link Consumer.Context} for manual message settlement. + * @return the {@link Message} mapped from a {@link com.rabbitmq.client.amqp.Message}. + */ + public static Message fromAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, + Consumer.@Nullable Context context) { + + MessageProperties messageProperties = new MessageProperties(); + + JavaUtils.INSTANCE + .acceptIfNotNull(amqpMessage.messageIdAsString(), messageProperties::setMessageId) + .acceptIfNotNull(amqpMessage.userId(), + (usr) -> messageProperties.setUserId(new String(usr, StandardCharsets.UTF_8))) + .acceptIfNotNull(amqpMessage.correlationIdAsString(), messageProperties::setCorrelationId) + .acceptIfNotNull(amqpMessage.contentType(), messageProperties::setContentType) + .acceptIfNotNull(amqpMessage.contentEncoding(), messageProperties::setContentEncoding) + .acceptIfNotNull(amqpMessage.absoluteExpiryTime(), + (exp) -> messageProperties.setExpiration(Long.toString(exp))) + .acceptIfNotNull(amqpMessage.creationTime(), (time) -> messageProperties.setTimestamp(new Date(time))) + .acceptIfNotNull(amqpMessage.replyTo(), messageProperties::setReplyTo); + + amqpMessage.forEachProperty(messageProperties::setHeader); + + if (context != null) { + messageProperties.setAmqpAcknowledgment((status) -> { + switch (status) { + case ACCEPT -> context.accept(); + case REJECT -> context.discard(); + case REQUEUE -> context.requeue(); + } + }); + } + + return new Message(amqpMessage.body(), messageProperties); + } + + /** + * Convert {@link Message} into {@link com.rabbitmq.client.amqp.Message}. + * The {@link MessageProperties#getReplyTo()} is set into {@link com.rabbitmq.client.amqp.Message#to(String)}. + * The {@link com.rabbitmq.client.amqp.Message#correlationId(long)} is set to + * {@link MessageProperties#getCorrelationId()} if present, or to {@link MessageProperties#getMessageId()}. + * @param message the {@link Message} convert from. + * @param amqpMessage the {@link com.rabbitmq.client.amqp.Message} convert into. + */ + public static void toAmqpMessage(Message message, com.rabbitmq.client.amqp.Message amqpMessage) { + MessageProperties messageProperties = message.getMessageProperties(); + + amqpMessage + .body(message.getBody()) + .contentEncoding(messageProperties.getContentEncoding()) + .contentType(messageProperties.getContentType()) + .messageId(messageProperties.getMessageId()) + .priority(messageProperties.getPriority().byteValue()); + + Map headers = messageProperties.getHeaders(); + if (!headers.isEmpty()) { + headers.forEach((key, val) -> mapProp(key, val, amqpMessage)); + } + + JavaUtils.INSTANCE + .acceptOrElseIfNotNull(messageProperties.getCorrelationId(), + messageProperties.getMessageId(), amqpMessage::correlationId) + .acceptIfNotNull(messageProperties.getUserId(), + (userId) -> amqpMessage.userId(userId.getBytes(StandardCharsets.UTF_8))) + .acceptIfNotNull(messageProperties.getReplyTo(), amqpMessage::to) + .acceptIfNotNull(messageProperties.getTimestamp(), + (timestamp) -> amqpMessage.creationTime(timestamp.getTime())) + .acceptIfNotNull(messageProperties.getExpiration(), + (expiration) -> amqpMessage.absoluteExpiryTime(Long.parseLong(expiration))); + } + + private static void mapProp(String key, @Nullable Object val, com.rabbitmq.client.amqp.Message amqpMessage) { + if (val == null) { + return; + } + if (val instanceof String string) { + amqpMessage.property(key, string); + } + else if (val instanceof Long longValue) { + amqpMessage.property(key, longValue); + } + else if (val instanceof Integer intValue) { + amqpMessage.property(key, intValue); + } + else if (val instanceof Short shortValue) { + amqpMessage.property(key, shortValue); + } + else if (val instanceof Byte byteValue) { + amqpMessage.property(key, byteValue); + } + else if (val instanceof Double doubleValue) { + amqpMessage.property(key, doubleValue); + } + else if (val instanceof Float floatValue) { + amqpMessage.property(key, floatValue); + } + else if (val instanceof Character character) { + amqpMessage.property(key, character); + } + else if (val instanceof UUID uuid) { + amqpMessage.property(key, uuid); + } + else if (val instanceof byte[] bytes) { + amqpMessage.property(key, bytes); + } + else if (val instanceof Boolean booleanValue) { + amqpMessage.property(key, booleanValue); + } + } + + private RabbitAmqpUtils() { + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/SingleAmqpConnectionFactory.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/SingleAmqpConnectionFactory.java new file mode 100644 index 0000000000..c9d0c188ab --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/SingleAmqpConnectionFactory.java @@ -0,0 +1,302 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client; + +import java.time.Duration; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +import javax.net.ssl.SSLContext; + +import com.rabbitmq.client.amqp.AddressSelector; +import com.rabbitmq.client.amqp.BackOffDelayPolicy; +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.ConnectionBuilder; +import com.rabbitmq.client.amqp.ConnectionSettings; +import com.rabbitmq.client.amqp.CredentialsProvider; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.OAuth2Settings; +import com.rabbitmq.client.amqp.Resource; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.DisposableBean; + +/** + * The {@link AmqpConnectionFactory} implementation to hold a single, shared {@link Connection} instance. + * + * @author Artem Bilan + * + * @since 4.0 + */ +public class SingleAmqpConnectionFactory implements AmqpConnectionFactory, DisposableBean { + + private final ConnectionBuilder connectionBuilder; + + private final Lock instanceLock = new ReentrantLock(); + + private volatile @Nullable Connection connection; + + public SingleAmqpConnectionFactory(Environment amqpEnvironment) { + this.connectionBuilder = amqpEnvironment.connectionBuilder(); + } + + public SingleAmqpConnectionFactory setHost(String host) { + this.connectionBuilder.host(host); + return this; + } + + public SingleAmqpConnectionFactory setPort(int port) { + this.connectionBuilder.port(port); + return this; + } + + public SingleAmqpConnectionFactory setUsername(String username) { + this.connectionBuilder.username(username); + return this; + } + + public SingleAmqpConnectionFactory setPassword(String password) { + this.connectionBuilder.password(password); + return this; + } + + public SingleAmqpConnectionFactory setVirtualHost(String virtualHost) { + this.connectionBuilder.virtualHost(virtualHost); + return this; + } + + public SingleAmqpConnectionFactory setUri(String uri) { + this.connectionBuilder.uri(uri); + return this; + } + + public SingleAmqpConnectionFactory setUris(String... uris) { + this.connectionBuilder.uris(uris); + return this; + } + + public SingleAmqpConnectionFactory setIdleTimeout(Duration idleTimeout) { + this.connectionBuilder.idleTimeout(idleTimeout); + return this; + } + + public SingleAmqpConnectionFactory setAddressSelector(AddressSelector addressSelector) { + this.connectionBuilder.addressSelector(addressSelector); + return this; + } + + public SingleAmqpConnectionFactory setCredentialsProvider(CredentialsProvider credentialsProvider) { + this.connectionBuilder.credentialsProvider(credentialsProvider); + return this; + } + + public SingleAmqpConnectionFactory setSaslMechanism(SaslMechanism saslMechanism) { + this.connectionBuilder.saslMechanism(saslMechanism.name()); + return this; + } + + public SingleAmqpConnectionFactory setTls(Consumer tlsCustomizer) { + tlsCustomizer.accept(new Tls(this.connectionBuilder.tls())); + return this; + } + + public SingleAmqpConnectionFactory setAffinity(Consumer affinityCustomizer) { + affinityCustomizer.accept(new Affinity(this.connectionBuilder.affinity())); + return this; + } + + public SingleAmqpConnectionFactory setOAuth2(Consumer oauth2Customizer) { + oauth2Customizer.accept(new OAuth2(this.connectionBuilder.oauth2())); + return this; + } + + public SingleAmqpConnectionFactory setRecovery(Consumer recoveryCustomizer) { + recoveryCustomizer.accept(new Recovery(this.connectionBuilder.recovery())); + return this; + } + + public SingleAmqpConnectionFactory setListeners(Resource.StateListener... listeners) { + this.connectionBuilder.listeners(listeners); + return this; + } + + @Override + public Connection getConnection() { + Connection connectionToReturn = this.connection; + if (connectionToReturn == null) { + this.instanceLock.lock(); + try { + connectionToReturn = this.connection; + if (connectionToReturn == null) { + connectionToReturn = this.connectionBuilder.build(); + this.connection = connectionToReturn; + } + } + finally { + this.instanceLock.unlock(); + } + } + return connectionToReturn; + } + + @Override + public void destroy() { + Connection connectionToClose = this.connection; + if (connectionToClose != null) { + connectionToClose.close(); + this.connection = null; + } + } + + public enum SaslMechanism { + + PLAIN, ANONYMOUS, EXTERNAL + + } + + public static final class Tls { + + private final ConnectionSettings.TlsSettings tls; + + private Tls(ConnectionSettings.TlsSettings tls) { + this.tls = tls; + } + + public Tls hostnameVerification() { + this.tls.hostnameVerification(); + return this; + } + + public Tls hostnameVerification(boolean hostnameVerification) { + this.tls.hostnameVerification(hostnameVerification); + return this; + } + + public Tls sslContext(SSLContext sslContext) { + this.tls.sslContext(sslContext); + return this; + } + + public Tls trustEverything() { + this.tls.trustEverything(); + return this; + } + + } + + public static final class Affinity { + + private final ConnectionSettings.Affinity affinity; + + private Affinity(ConnectionSettings.Affinity affinity) { + this.affinity = affinity; + } + + public Affinity queue(String queue) { + this.affinity.queue(queue); + return this; + } + + public Affinity operation(ConnectionSettings.Affinity.Operation operation) { + this.affinity.operation(operation); + return this; + } + + public Affinity reuse(boolean reuse) { + this.affinity.reuse(reuse); + return this; + } + + public Affinity strategy(ConnectionSettings.AffinityStrategy strategy) { + this.affinity.strategy(strategy); + return this; + } + + } + + public static final class OAuth2 { + + private final OAuth2Settings oAuth2Settings; + + private OAuth2(OAuth2Settings oAuth2Settings) { + this.oAuth2Settings = oAuth2Settings; + } + + public OAuth2 tokenEndpointUri(String uri) { + this.oAuth2Settings.tokenEndpointUri(uri); + return this; + } + + public OAuth2 clientId(String clientId) { + this.oAuth2Settings.clientId(clientId); + return this; + } + + public OAuth2 clientSecret(String clientSecret) { + this.oAuth2Settings.clientSecret(clientSecret); + return this; + } + + public OAuth2 grantType(String grantType) { + this.oAuth2Settings.grantType(grantType); + return this; + } + + public OAuth2 parameter(String name, String value) { + this.oAuth2Settings.parameter(name, value); + return this; + } + + public OAuth2 shared(boolean shared) { + this.oAuth2Settings.shared(shared); + return this; + } + + public OAuth2 sslContext(SSLContext sslContext) { + this.oAuth2Settings.tls().sslContext(sslContext); + return this; + } + + } + + public static final class Recovery { + + private final ConnectionBuilder.RecoveryConfiguration recoveryConfiguration; + + private Recovery(ConnectionBuilder.RecoveryConfiguration recoveryConfiguration) { + this.recoveryConfiguration = recoveryConfiguration; + } + + public Recovery activated(boolean activated) { + this.recoveryConfiguration.activated(activated); + return this; + } + + public Recovery backOffDelayPolicy(BackOffDelayPolicy backOffDelayPolicy) { + this.recoveryConfiguration.backOffDelayPolicy(backOffDelayPolicy); + return this; + } + + public Recovery topology(boolean activated) { + this.recoveryConfiguration.topology(activated); + return this; + } + + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java new file mode 100644 index 0000000000..17235a0449 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client.config; + +import java.util.Arrays; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.rabbit.config.BaseRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbit.listener.MethodRabbitListenerEndpoint; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpMessageListenerAdapter; +import org.springframework.amqp.utils.JavaUtils; +import org.springframework.scheduling.TaskScheduler; + +/** + * Factory for {@link RabbitAmqpListenerContainer}. + * To use it as default one, has to be configured with a + * {@link org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor#DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME}. + * + * @author Artem Bilan + * + * @since 4.0 + * + */ +public class RabbitAmqpListenerContainerFactory + extends BaseRabbitListenerContainerFactory { + + private final AmqpConnectionFactory connectionFactory; + + private @Nullable ContainerCustomizer containerCustomizer; + + private MessagePostProcessor @Nullable [] afterReceivePostProcessors; + + private @Nullable Integer batchSize; + + private @Nullable Long batchReceiveTimeout; + + private @Nullable TaskScheduler taskScheduler; + + /** + * Construct an instance using the provided {@link AmqpConnectionFactory}. + * @param connectionFactory the connection. + */ + public RabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + /** + * Set a {@link ContainerCustomizer} that is invoked after a container is created and + * configured to enable further customization of the container. + * @param containerCustomizer the customizer. + */ + public void setContainerCustomizer(ContainerCustomizer containerCustomizer) { + this.containerCustomizer = containerCustomizer; + } + + /** + * Set {@link MessagePostProcessor}s that will be applied after message reception, before + * invoking the {@link MessageListener}. Often used to decompress data. Processors are invoked in order, + * depending on {@code PriorityOrder}, {@code Order} and finally unordered. + * @param afterReceivePostProcessors the post processor. + */ + public void setAfterReceivePostProcessors(MessagePostProcessor... afterReceivePostProcessors) { + this.afterReceivePostProcessors = Arrays.copyOf(afterReceivePostProcessors, afterReceivePostProcessors.length); + } + + /** + * The size of the batch of messages to process. + * This is only option (if {@code batchSize > 1}) which turns the target listener container into a batch mode. + * @param batchSize the batch size. + * @see RabbitAmqpListenerContainer#setBatchSize + * @see #setBatchReceiveTimeout(Long) + */ + public void setBatchSize(Integer batchSize) { + this.batchSize = batchSize; + } + + /** + * The number of milliseconds of timeout for gathering batch messages. + * It limits the time to wait to fill batchSize. + * Default is 30 seconds. + * @param batchReceiveTimeout the timeout for gathering batch messages. + * @see RabbitAmqpListenerContainer#setBatchReceiveTimeout + * @see #setBatchSize(Integer) + */ + public void setBatchReceiveTimeout(Long batchReceiveTimeout) { + this.batchReceiveTimeout = batchReceiveTimeout; + } + + /** + * Configure a {@link TaskScheduler} to release not fulfilled batches after timeout. + * @param taskScheduler the {@link TaskScheduler} to use. + * @see RabbitAmqpListenerContainer#setTaskScheduler(TaskScheduler) + * @see #setBatchReceiveTimeout(Long) + */ + public void setTaskScheduler(TaskScheduler taskScheduler) { + this.taskScheduler = taskScheduler; + } + + @Override + public RabbitAmqpListenerContainer createListenerContainer(@Nullable RabbitListenerEndpoint endpoint) { + if (endpoint instanceof MethodRabbitListenerEndpoint methodRabbitListenerEndpoint) { + JavaUtils.INSTANCE + .acceptIfCondition(this.batchSize != null && this.batchSize > 1, + true, + methodRabbitListenerEndpoint::setBatchListener); + + methodRabbitListenerEndpoint.setAdapterProvider( + (batch, bean, method, returnExceptions, errorHandler, batchingStrategy) -> + new RabbitAmqpMessageListenerAdapter(bean, method, returnExceptions, errorHandler, batch)); + } + RabbitAmqpListenerContainer container = createContainerInstance(); + JavaUtils.INSTANCE + .acceptIfNotNull(getAdviceChain(), container::setAdviceChain) + .acceptIfNotNull(getDefaultRequeueRejected(), container::setDefaultRequeue) + .acceptIfNotNull(this.afterReceivePostProcessors, container::setAfterReceivePostProcessors) + .acceptIfNotNull(this.batchSize, container::setBatchSize) + .acceptIfNotNull(this.batchReceiveTimeout, container::setBatchReceiveTimeout) + .acceptIfNotNull(this.taskScheduler, container::setTaskScheduler); + + applyCommonOverrides(endpoint, container); + + if (endpoint != null) { + JavaUtils.INSTANCE + .acceptIfNotNull(endpoint.getAckMode(), + (ackMode) -> container.setAutoSettle(!ackMode.isManual())) + .acceptIfNotNull(endpoint.getConcurrency(), + (concurrency) -> container.setConsumersPerQueue(Integer.parseInt(concurrency))); + } + if (this.containerCustomizer != null) { + this.containerCustomizer.configure(container); + } + return container; + } + + protected RabbitAmqpListenerContainer createContainerInstance() { + return new RabbitAmqpListenerContainer(this.connectionFactory); + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/package-info.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/package-info.java new file mode 100644 index 0000000000..3b874dd25f --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides classes for Spring application context support. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.rabbitmq.client.config; \ No newline at end of file diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java new file mode 100644 index 0000000000..1686051fc5 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java @@ -0,0 +1,586 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client.listener; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Resource; +import org.aopalliance.aop.Advice; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.core.AmqpAcknowledgment; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.support.ContainerUtils; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpUtils; +import org.springframework.amqp.support.postprocessor.MessagePostProcessorUtils; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.core.log.LogAccessor; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.util.Assert; +import org.springframework.util.ErrorHandler; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; + +/** + * A listener container for RabbitMQ AMQP 1.0 Consumer. + * + * @author Artem Bilan + * + * @since 4.0 + * + */ +public class RabbitAmqpListenerContainer implements MessageListenerContainer, BeanNameAware, DisposableBean { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(RabbitAmqpListenerContainer.class)); + + private final Lock lock = new ReentrantLock(); + + private final AmqpConnectionFactory connectionFactory; + + private final MultiValueMap queueToConsumers = new LinkedMultiValueMap<>(); + + private String @Nullable [] queues; + + private Advice @Nullable [] adviceChain; + + private int initialCredits = 100; + + private int priority; + + private Resource.StateListener @Nullable [] stateListeners; + + private boolean autoSettle = true; + + private boolean defaultRequeue = true; + + private int consumersPerQueue = 1; + + private @Nullable MessageListener messageListener; + + private @Nullable MessageListener proxy; + + private boolean asyncReplies; + + private ErrorHandler errorHandler = new ConditionalRejectingErrorHandler(); + + private @Nullable Collection afterReceivePostProcessors; + + private boolean autoStartup = true; + + private String beanName = "not.a.Spring.bean"; + + private @Nullable String listenerId; + + private Duration gracefulShutdownPeriod = Duration.ofSeconds(30); + + private int batchSize; + + private Duration batchReceiveDuration = Duration.ofSeconds(30); + + private @Nullable TaskScheduler taskScheduler; + + private boolean internalTaskScheduler = true; + + /** + * Construct an instance based on the provided {@link AmqpConnectionFactory}. + * @param connectionFactory to use. + */ + public RabbitAmqpListenerContainer(AmqpConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + @Override + public void setQueueNames(String... queueNames) { + this.queues = Arrays.copyOf(queueNames, queueNames.length); + } + + public void setInitialCredits(int initialCredits) { + this.initialCredits = initialCredits; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public void setStateListeners(Resource.StateListener... stateListeners) { + this.stateListeners = Arrays.copyOf(stateListeners, stateListeners.length); + } + + /** + * Set {@link MessagePostProcessor}s that will be applied after message reception, before + * invoking the {@link MessageListener}. Often used to decompress data. Processors are invoked in order, + * depending on {@code PriorityOrder}, {@code Order} and finally unordered. + * @param afterReceivePostProcessors the post processor. + */ + public void setAfterReceivePostProcessors(MessagePostProcessor... afterReceivePostProcessors) { + this.afterReceivePostProcessors = MessagePostProcessorUtils.sort(Arrays.asList(afterReceivePostProcessors)); + } + + public void setBatchSize(int batchSize) { + Assert.isTrue(batchSize > 1, "'batchSize' must be greater than 1"); + this.batchSize = batchSize; + } + + public void setBatchReceiveTimeout(long batchReceiveTimeout) { + this.batchReceiveDuration = Duration.ofMillis(batchReceiveTimeout); + } + + /** + * Set a {@link TaskScheduler} for monitoring batch releases. + * @param taskScheduler the {@link TaskScheduler} to use. + */ + public void setTaskScheduler(TaskScheduler taskScheduler) { + this.taskScheduler = taskScheduler; + this.internalTaskScheduler = false; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void setAutoStartup(boolean autoStart) { + this.autoStartup = autoStart; + } + + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + /** + * Set an advice chain to apply to the listener. + * @param advices the advice chain. + * @since 2.4.5 + */ + public void setAdviceChain(Advice... advices) { + Assert.notNull(advices, "'advices' cannot be null"); + Assert.noNullElements(advices, "'advices' cannot have null elements"); + this.adviceChain = Arrays.copyOf(advices, advices.length); + } + + /** + * Set to {@code false} to propagate a + * {@link org.springframework.amqp.core.MessageProperties#setAmqpAcknowledgment(AmqpAcknowledgment)} + * for target {@link MessageListener} manual settlement. + * In case of {@link RabbitAmqpMessageListener}, the native {@link Consumer.Context} + * should be used for manual settlement. + * @param autoSettle to call {@link Consumer.Context#accept()} automatically. + */ + public void setAutoSettle(boolean autoSettle) { + this.autoSettle = autoSettle; + } + + /** + * Set the default behavior when a message processing has failed. + * When true, messages will be requeued, when false, they will be discarded. + * When true, the default can be overridden by the listener throwing an + * {@link AmqpRejectAndDontRequeueException}. Default true. + * @param defaultRequeue true to requeue by default. + */ + public void setDefaultRequeue(boolean defaultRequeue) { + this.defaultRequeue = defaultRequeue; + } + + public void setGracefulShutdownPeriod(Duration gracefulShutdownPeriod) { + this.gracefulShutdownPeriod = gracefulShutdownPeriod; + } + + /** + * Each queue runs in its own consumer; set this property to create multiple + * consumers for each queue. + * Can be treated as {@code concurrency}, but per queue. + * @param consumersPerQueue the consumers per queue. + */ + public void setConsumersPerQueue(int consumersPerQueue) { + this.consumersPerQueue = consumersPerQueue; + } + + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + @Override + public void setListenerId(String id) { + this.listenerId = id; + } + + /** + * The 'id' attribute of the listener. + * @return the id (or the container bean name if no id set). + */ + public String getListenerId() { + return this.listenerId != null ? this.listenerId : this.beanName; + } + + @Override + public void setupMessageListener(MessageListener messageListener) { + this.messageListener = messageListener; + this.asyncReplies = messageListener.isAsyncReplies(); + if (this.messageListener instanceof RabbitAmqpMessageListenerAdapter rabbitAmqpMessageListenerAdapter) { + rabbitAmqpMessageListenerAdapter.setConnectionFactory(this.connectionFactory); + } + this.proxy = this.messageListener; + if (!ObjectUtils.isEmpty(this.adviceChain)) { + ProxyFactory factory = new ProxyFactory(messageListener); + for (Advice advice : this.adviceChain) { + factory.addAdvisor(new DefaultPointcutAdvisor(advice)); + } + factory.setInterfaces(messageListener.getClass().getInterfaces()); + this.proxy = (MessageListener) factory.getProxy(getClass().getClassLoader()); + } + } + + @Override + public @Nullable Object getMessageListener() { + return this.proxy; + } + + @Override + public void afterPropertiesSet() { + Assert.state(this.queues != null, "At least one queue has to be provided for consuming."); + Assert.state(this.messageListener != null, "The 'messageListener' must be provided."); + + if (this.asyncReplies && this.autoSettle) { + LOG.info("Enforce MANUAL settlement for async replies."); + this.autoSettle = false; + } + + this.messageListener.containerAckMode(this.autoSettle ? AcknowledgeMode.AUTO : AcknowledgeMode.MANUAL); + if (this.messageListener instanceof RabbitAmqpMessageListenerAdapter adapter + && this.afterReceivePostProcessors != null) { + + adapter.setAfterReceivePostProcessors(this.afterReceivePostProcessors); + } + + if (this.batchSize > 1 && this.internalTaskScheduler) { + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setThreadNamePrefix(getListenerId() + "-consumerMonitor-"); + threadPoolTaskScheduler.afterPropertiesSet(); + this.taskScheduler = threadPoolTaskScheduler; + } + } + + @Override + public boolean isRunning() { + this.lock.lock(); + try { + return !this.queueToConsumers.isEmpty(); + } + finally { + this.lock.unlock(); + } + } + + @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public void start() { + this.lock.lock(); + try { + if (this.queueToConsumers.isEmpty()) { + Connection connection = this.connectionFactory.getConnection(); + for (String queue : this.queues) { + for (int i = 0; i < this.consumersPerQueue; i++) { + Consumer consumer = + connection.consumerBuilder() + .queue(queue) + .priority(this.priority) + .initialCredits(this.initialCredits) + .listeners(this.stateListeners) + .messageHandler(new ConsumerMessageHandler()) + .build(); + this.queueToConsumers.add(queue, consumer); + } + } + } + } + finally { + this.lock.unlock(); + } + } + + private void invokeListener(Consumer.Context context, com.rabbitmq.client.amqp.Message amqpMessage) { + try { + doInvokeListener(context, amqpMessage); + if (this.autoSettle) { + context.accept(); + } + } + catch (Exception ex) { + handleListenerError(ex, context, amqpMessage); + } + } + + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private void doInvokeListener(Consumer.Context context, com.rabbitmq.client.amqp.Message amqpMessage) { + Consumer.@Nullable Context contextToUse = this.autoSettle ? null : context; + if (this.proxy instanceof RabbitAmqpMessageListener amqpMessageListener) { + amqpMessageListener.onAmqpMessage(amqpMessage, contextToUse); + } + else { + Message message = RabbitAmqpUtils.fromAmqpMessage(amqpMessage, contextToUse); + this.proxy.onMessage(message); + } + } + + private void invokeBatchListener(Consumer.Context context, List batch) { + Consumer.@Nullable Context contextToUse = this.autoSettle ? null : context; + List messages = + batch.stream() + .map((amqpMessage) -> RabbitAmqpUtils.fromAmqpMessage(amqpMessage, contextToUse)) + .toList(); + try { + doInvokeBatchListener(messages); + if (this.autoSettle) { + context.accept(); + } + } + catch (Exception ex) { + handleListenerError(ex, context, batch); + } + } + + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private void doInvokeBatchListener(List messages) { + this.proxy.onMessageBatch(messages); + } + + private void handleListenerError(Exception ex, Consumer.Context context, Object messageOrBatch) { + try { + this.errorHandler.handleError(ex); + // If error handler does not re-throw an exception, re-check original error. + // If it is not special, treat the error handler outcome as a successful processing result. + if (!handleSpecialErrors(ex, context)) { + context.accept(); + } + } + catch (Exception rethrow) { + if (!handleSpecialErrors(rethrow, context)) { + if (this.defaultRequeue) { + context.requeue(); + } + else { + context.discard(); + } + LOG.error(rethrow, () -> + "The 'errorHandler' has thrown an exception. The '" + messageOrBatch + "' is " + + (this.defaultRequeue ? "re-queued." : "discarded.")); + } + } + } + + private boolean handleSpecialErrors(Exception ex, Consumer.Context context) { + if (ContainerUtils.shouldRequeue(this.defaultRequeue, ex, LOG.getLog())) { + context.requeue(); + return true; + } + if (ContainerUtils.isAmqpReject(ex)) { + context.discard(); + return true; + } + if (ContainerUtils.isImmediateAcknowledge(ex)) { + context.accept(); + return true; + } + return false; + } + + @Override + public void stop() { + stop(() -> { + }); + } + + @Override + @SuppressWarnings("unchecked") + public void stop(Runnable callback) { + this.lock.lock(); + try { + CompletableFuture[] completableFutures = + this.queueToConsumers.values().stream() + .flatMap(List::stream) + .map((consumer) -> + CompletableFuture.supplyAsync(() -> { + consumer.pause(); + try (consumer) { + while (consumer.unsettledMessageCount() > 0) { + Thread.sleep(100); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException(ex); + } + return null; + })) + .toArray(CompletableFuture[]::new); + + CompletableFuture.allOf(completableFutures) + .orTimeout(this.gracefulShutdownPeriod.toMillis(), TimeUnit.MILLISECONDS) + .whenComplete((unused, throwable) -> { + this.queueToConsumers.clear(); + callback.run(); + }); + } + finally { + this.lock.unlock(); + } + } + + /** + * Pause all the consumer for all queues. + */ + public void pause() { + this.queueToConsumers.values() + .stream() + .flatMap(List::stream) + .forEach(Consumer::pause); + } + + /** + * Resume all the consumer for all queues. + */ + public void resume() { + this.queueToConsumers.values() + .stream() + .flatMap(List::stream) + .forEach(Consumer::unpause); + } + + /** + * Pause all the consumer for specific queue. + */ + public void pause(String queueName) { + List consumers = this.queueToConsumers.get(queueName); + if (consumers != null) { + consumers.forEach(Consumer::pause); + } + } + + /** + * Resume all the consumer for specific queue. + */ + public void resume(String queueName) { + List consumers = this.queueToConsumers.get(queueName); + if (consumers != null) { + consumers.forEach(Consumer::unpause); + } + } + + @Override + public void destroy() { + if (this.internalTaskScheduler && this.taskScheduler != null) { + ((ThreadPoolTaskScheduler) this.taskScheduler).shutdown(); + } + } + + private class ConsumerMessageHandler implements Consumer.MessageHandler { + + private volatile @Nullable ConsumerBatch consumerBatch; + + ConsumerMessageHandler() { + } + + @Override + public void handle(Consumer.Context context, com.rabbitmq.client.amqp.Message message) { + if (RabbitAmqpListenerContainer.this.batchSize > 1) { + ConsumerBatch currentBatch = this.consumerBatch; + if (currentBatch == null || currentBatch.batchReleaseFuture == null) { + currentBatch = new ConsumerBatch(context.batch(RabbitAmqpListenerContainer.this.batchSize)); + this.consumerBatch = currentBatch; + } + currentBatch.add(context, message); + if (currentBatch.batchContext.size() == RabbitAmqpListenerContainer.this.batchSize) { + currentBatch.release(); + this.consumerBatch = null; + } + } + else { + invokeListener(context, message); + } + } + + private class ConsumerBatch { + + private final List batch = new ArrayList<>(); + + private final Consumer.BatchContext batchContext; + + private volatile @Nullable ScheduledFuture batchReleaseFuture; + + ConsumerBatch(Consumer.BatchContext batchContext) { + this.batchContext = batchContext; + } + + void add(Consumer.Context context, com.rabbitmq.client.amqp.Message message) { + this.batchContext.add(context); + this.batch.add(message); + if (this.batchReleaseFuture == null) { + this.batchReleaseFuture = + Objects.requireNonNull(RabbitAmqpListenerContainer.this.taskScheduler) + .schedule(this::releaseInternal, + Instant.now().plus(RabbitAmqpListenerContainer.this.batchReceiveDuration)); + } + } + + void release() { + ScheduledFuture currentBatchReleaseFuture = this.batchReleaseFuture; + if (currentBatchReleaseFuture != null) { + currentBatchReleaseFuture.cancel(true); + releaseInternal(); + } + } + + private void releaseInternal() { + if (this.batchReleaseFuture != null) { + this.batchReleaseFuture = null; + invokeBatchListener(this.batchContext, this.batch); + } + } + + } + + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListener.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListener.java new file mode 100644 index 0000000000..70cc52cbf6 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListener.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client.listener; + +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Message; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.MessageListener; + +/** + * A message listener that receives native AMQP 1.0 messages from RabbitMQ. + * + * @author Artem Bilan + * + * @since 4.0 + */ +public interface RabbitAmqpMessageListener extends MessageListener { + + /** + * Process an AMQP message. + * @param message the message to process. + * @param context the consumer context to settle message. + * Null if container is configured for {@code autoSettle}. + */ + void onAmqpMessage(Message message, Consumer.@Nullable Context context); + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java new file mode 100644 index 0000000000..123e1b403d --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java @@ -0,0 +1,214 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client.listener; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.amqp.Consumer; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Address; +import org.springframework.amqp.core.AmqpAcknowledgment; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.rabbit.listener.adapter.InvocationResult; +import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; +import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; +import org.springframework.amqp.rabbit.listener.support.ContainerUtils; +import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpUtils; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A {@link MessagingMessageListenerAdapter} extension for the {@link RabbitAmqpMessageListener}. + * Provides these arguments for the {@link #getHandlerAdapter()} invocation: + *
    + *
  • {@link com.rabbitmq.client.amqp.Message} - the native AMQP 1.0 message without any conversions
  • + *
  • {@link org.springframework.amqp.core.Message} - Spring AMQP message abstraction as conversion result from the native AMQP 1.0 message
  • + *
  • {@link org.springframework.messaging.Message} - Spring Messaging abstraction as conversion result from the Spring AMQP message
  • + *
  • {@link Consumer.Context} - RabbitMQ AMQP client consumer settlement API.
  • + *
  • {@link org.springframework.amqp.core.AmqpAcknowledgment} - Spring AMQP acknowledgment abstraction: delegates to the {@link Consumer.Context}
  • + *
+ *

+ * This class reuses the {@link MessagingMessageListenerAdapter} as much as possible just to avoid duplication. + * The {@link Channel} abstraction from AMQP Client 0.9.1 is out use and present here just for API compatibility + * and to follow DRY principle. + * Can be reworked eventually, when this AMQP 1.0 client won't be based on {@code spring-rabbit} dependency. + * + * @author Artem Bilan + * + * @since 4.0 + */ +public class RabbitAmqpMessageListenerAdapter extends MessagingMessageListenerAdapter + implements RabbitAmqpMessageListener { + + private @Nullable Collection afterReceivePostProcessors; + + private @Nullable RabbitAmqpTemplate rabbitAmqpTemplate; + + public RabbitAmqpMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler, boolean batch) { + + super(bean, method, returnExceptions, errorHandler, batch); + } + + public void setAfterReceivePostProcessors(Collection afterReceivePostProcessors) { + this.afterReceivePostProcessors = new ArrayList<>(afterReceivePostProcessors); + } + + /** + * Set a {@link AmqpConnectionFactory} for publishing replies from this adapter. + * @param connectionFactory the {@link AmqpConnectionFactory} for replies. + */ + public void setConnectionFactory(AmqpConnectionFactory connectionFactory) { + this.rabbitAmqpTemplate = new RabbitAmqpTemplate(connectionFactory); + } + + @Override + public void onAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, Consumer.@Nullable Context context) { + org.springframework.amqp.core.Message springMessage = RabbitAmqpUtils.fromAmqpMessage(amqpMessage, context); + if (this.afterReceivePostProcessors != null) { + for (MessagePostProcessor processor : this.afterReceivePostProcessors) { + springMessage = processor.postProcessMessage(springMessage); + } + } + try { + org.springframework.messaging.Message messagingMessage = toMessagingMessage(springMessage); + InvocationResult result = getHandlerAdapter() + .invoke(messagingMessage, + springMessage, springMessage.getMessageProperties().getAmqpAcknowledgment(), + amqpMessage, context); + + if (result.getReturnValue() != null) { + Assert.notNull(this.rabbitAmqpTemplate, + "The 'connectionFactory' must be provided for handling replies."); + handleResult(result, springMessage, null, messagingMessage); + } + + } + catch (Exception ex) { + throw new ListenerExecutionFailedException("Failed to invoke listener", ex, springMessage); + } + } + + @Override + protected void asyncFailure(Message request, @Nullable Channel channel, Throwable t, @Nullable Object source) { + try { + handleException(request, channel, (org.springframework.messaging.Message) source, + new ListenerExecutionFailedException("Async Fail", t, request)); + return; + } + catch (Exception ex) { + // Ignore and reject the message against original error + } + + this.logger.error("Future, Mono, or suspend function was completed with an exception for " + request, t); + AmqpAcknowledgment amqpAcknowledgment = request.getMessageProperties().getAmqpAcknowledgment(); + Assert.notNull(amqpAcknowledgment, "'(amqpAcknowledgment' must be provided into request message."); + + if (ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), t, this.logger)) { + amqpAcknowledgment.acknowledge(AmqpAcknowledgment.Status.REQUEUE); + } + else { + amqpAcknowledgment.acknowledge(AmqpAcknowledgment.Status.REJECT); + } + } + + @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected void sendResponse(@Nullable Channel channel, Address replyTo, Message messageIn) { + Message replyMessage = messageIn; + MessagePostProcessor[] beforeSendReplyPostProcessors = getBeforeSendReplyPostProcessors(); + if (beforeSendReplyPostProcessors != null) { + for (MessagePostProcessor postProcessor : beforeSendReplyPostProcessors) { + replyMessage = postProcessor.postProcessMessage(replyMessage); + } + } + + String replyToExchange = replyTo.getExchangeName(); + String replyToRoutingKey = replyTo.getRoutingKey(); + CompletableFuture sendFuture; + if (StringUtils.hasText(replyToExchange)) { + sendFuture = this.rabbitAmqpTemplate.send(replyToExchange, replyToRoutingKey, replyMessage); + } + else { + Assert.hasText(replyToRoutingKey, "The 'replyTo' must be provided, in request message or in @SendTo."); + sendFuture = this.rabbitAmqpTemplate.send(replyToRoutingKey.replaceFirst("queues/", ""), replyMessage); + } + + sendFuture.join(); + } + + @Override + protected void basicAck(Message request, @Nullable Channel channel) { + AmqpAcknowledgment amqpAcknowledgment = request.getMessageProperties().getAmqpAcknowledgment(); + Assert.notNull(amqpAcknowledgment, "'(amqpAcknowledgment' must be provided into request message."); + amqpAcknowledgment.acknowledge(); + } + + @Override + public void onMessageBatch(List messages) { + AmqpAcknowledgment amqpAcknowledgment = + messages.stream() + .findAny() + .map((message) -> message.getMessageProperties().getAmqpAcknowledgment()) + .orElse(null); + + org.springframework.messaging.Message converted; + if (this.messagingMessageConverter.isAmqpMessageList()) { + converted = new GenericMessage<>(messages); + } + else { + List> messagingMessages = + messages.stream() + .map(this::toMessagingMessage) + .toList(); + + if (this.messagingMessageConverter.isMessageList()) { + converted = new GenericMessage<>(messagingMessages); + } + else { + List payloads = new ArrayList<>(); + for (org.springframework.messaging.Message message : messagingMessages) { + payloads.add(message.getPayload()); + } + converted = new GenericMessage<>(payloads); + } + } + try { + InvocationResult result = getHandlerAdapter() + .invoke(converted, amqpAcknowledgment); + if (result.getReturnValue() != null) { + logger.warn("Replies for batches are not currently supported with RabbitMQ AMQP 1.0 listeners"); + } + } + catch (Exception ex) { + throw new ListenerExecutionFailedException("Failed to invoke listener", ex, + messages.toArray(new Message[0])); + } + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/package-info.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/package-info.java new file mode 100644 index 0000000000..23d84893c6 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides Spring support for RabbitMQ AMQP 1.0 Consumer. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.rabbitmq.client.listener; \ No newline at end of file diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/package-info.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/package-info.java new file mode 100644 index 0000000000..ae2d6944fd --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides Spring support for RabbitMQ AMQP 1.0 Client. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.rabbitmq.client; \ No newline at end of file diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java new file mode 100644 index 0000000000..b8a045ab17 --- /dev/null +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.Queue; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Artem Bilan + * + * @since 4.0 + */ +@ContextConfiguration +public class RabbitAmqpAdminTests extends RabbitAmqpTestBase { + + @Autowired + @Qualifier("ds") + Declarables declarables; + + @Test + void verifyBeanDeclarations() { + CompletableFuture publishFutures = + CompletableFuture.allOf( + template.convertAndSend("e1", "k1", "test1"), + template.convertAndSend("e2", "k2", "test2"), + template.convertAndSend("e2", "k2", "test3"), + template.convertAndSend("e3", "k3", "test4"), + template.convertAndSend("e4", "k4", "test5")); + assertThat(publishFutures).succeedsWithin(Duration.ofSeconds(20)); + + assertThat(template.receiveAndConvert("q1")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test1"); + assertThat(template.receiveAndConvert("q2")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test2"); + assertThat(template.receiveAndConvert("q2")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test3"); + assertThat(template.receiveAndConvert("q3")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test4"); + assertThat(template.receiveAndConvert("q4")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test5"); + + assertThat(declarables.getDeclarablesByType(Queue.class)) + .hasSize(1) + .extracting(Queue::getName) + .contains("q4"); + assertThat(declarables.getDeclarablesByType(Exchange.class)) + .hasSize(1) + .extracting(Exchange::getName) + .contains("e4"); + assertThat(declarables.getDeclarablesByType(Binding.class)) + .hasSize(1) + .extracting(Binding::getDestination) + .contains("q4"); + } + + @Configuration + public static class Config { + + @Bean + DirectExchange e1() { + return new DirectExchange("e1"); + } + + @Bean + Queue q1() { + return new Queue("q1"); + } + + @Bean + Binding b1() { + return BindingBuilder.bind(q1()).to(e1()).with("k1"); + } + + @Bean + Declarables es() { + return new Declarables( + new DirectExchange("e2"), + new DirectExchange("e3")); + } + + @Bean + Declarables qs() { + return new Declarables( + new Queue("q2"), + new Queue("q3")); + } + + @Bean + Declarables bs() { + return new Declarables( + new Binding("q2", Binding.DestinationType.QUEUE, "e2", "k2", null), + new Binding("q3", Binding.DestinationType.QUEUE, "e3", "k3", null)); + } + + @Bean + Declarables ds() { + return new Declarables( + new DirectExchange("e4"), + new Queue("q4"), + new Binding("q4", Binding.DestinationType.QUEUE, "e4", "k4", null)); + } + + } + +} diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java new file mode 100644 index 0000000000..49f27bf60b --- /dev/null +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.AmqpIllegalStateException; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Artem Bilan + * + * @since 4.0 + */ +@ContextConfiguration +public class RabbitAmqpTemplateTests extends RabbitAmqpTestBase { + + RabbitAmqpTemplate rabbitAmqpTemplate; + + @BeforeEach + void setUp() { + this.rabbitAmqpTemplate = new RabbitAmqpTemplate(this.connectionFactory); + } + + @AfterEach + void tearDown() { + this.rabbitAmqpTemplate.destroy(); + } + + @Test + void illegalStateOnNoDefaults() { + assertThatIllegalStateException() + .isThrownBy(() -> this.template.send(new Message(new byte[0]))) + .withMessage( + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); + + assertThatIllegalStateException() + .isThrownBy(() -> this.template.convertAndSend(new byte[0])) + .withMessage( + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); + } + + @Test + void defaultExchangeAndRoutingKey() { + this.rabbitAmqpTemplate.setExchange("e1"); + this.rabbitAmqpTemplate.setRoutingKey("k1"); + + assertThat(this.rabbitAmqpTemplate.convertAndSend("test1")) + .succeedsWithin(Duration.ofSeconds(20)); + + assertThat(this.rabbitAmqpTemplate.receiveAndConvert("q1")) + .succeedsWithin(Duration.ofSeconds(20)) + .isEqualTo("test1"); + } + + @Test + void defaultQueues() { + this.rabbitAmqpTemplate.setQueue("q1"); + this.rabbitAmqpTemplate.setReceiveQueue("q1"); + + assertThat(this.rabbitAmqpTemplate.convertAndSend("test2")) + .succeedsWithin(Duration.ofSeconds(20)); + + assertThat(this.rabbitAmqpTemplate.receiveAndConvert()) + .succeedsWithin(Duration.ofSeconds(20)) + .isEqualTo("test2"); + } + + @Test + void verifyRpc() { + String testRequest = "rpc-request"; + String testReply = "rpc-reply"; + + CompletableFuture rpcClientResult = this.template.convertSendAndReceive("e1", "k1", testRequest); + + AtomicReference receivedRequest = new AtomicReference<>(); + CompletableFuture rpcServerResult = + this.rabbitAmqpTemplate.receiveAndReply("q1", + payload -> { + receivedRequest.set(payload); + return testReply; + }); + + assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(20)).isEqualTo(true); + assertThat(rpcClientResult).succeedsWithin(Duration.ofSeconds(20)).isEqualTo(testReply); + assertThat(receivedRequest.get()).isEqualTo(testRequest); + + this.template.send("q1", + MessageBuilder.withBody("non-rpc-request".getBytes(StandardCharsets.UTF_8)) + .setMessageId(UUID.randomUUID().toString()) + .setContentType(MimeTypeUtils.TEXT_PLAIN_VALUE) + .build()); + + rpcServerResult = this.rabbitAmqpTemplate.receiveAndReply("q1", payload -> "reply-attempt"); + + assertThat(rpcServerResult).failsWithin(Duration.ofSeconds(20)) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(AmqpIllegalStateException.class) + .withRootCauseInstanceOf(IllegalArgumentException.class) + .withMessageContaining("Failed to process RPC request: (Body:'non-rpc-request'") + .withStackTraceContaining("The 'reply-to' property has to be set on request. Used for reply publishing."); + + rpcClientResult = this.template.convertSendAndReceive("q1", testRequest); + rpcServerResult = this.rabbitAmqpTemplate.receiveAndReply("q1", payload -> null); + + assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(20)).isEqualTo(false); + assertThat(rpcClientResult).failsWithin(Duration.ofSeconds(2)) + .withThrowableThat() + .isInstanceOf(TimeoutException.class); + + this.template.convertSendAndReceive("q1", new byte[0]); + + rpcServerResult = this.rabbitAmqpTemplate.receiveAndReply("q1", payload -> payload); + assertThat(rpcServerResult).failsWithin(Duration.ofSeconds(20)) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(AmqpIllegalStateException.class) + .withRootCauseInstanceOf(ClassCastException.class) + .withMessageContaining("Failed to process RPC request: (Body:'[B") + .withStackTraceContaining("class [B cannot be cast to class java.lang.String"); + + assertThat(this.template.receiveAndConvert("dlq1")).succeedsWithin(20, TimeUnit.SECONDS) + .isEqualTo("non-rpc-request"); + + assertThat(this.template.receiveAndConvert("dlq1")).succeedsWithin(20, TimeUnit.SECONDS) + .isEqualTo(new byte[0]); + } + + @Configuration + static class Config { + + @Bean + DirectExchange e1() { + return new DirectExchange("e1"); + } + + @Bean + Queue q1() { + return QueueBuilder.durable("q1").deadLetterExchange("dlx1").build(); + } + + @Bean + Binding b1() { + return BindingBuilder.bind(q1()).to(e1()).with("k1"); + } + + } + +} diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java new file mode 100644 index 0000000000..d4ccbe0453 --- /dev/null +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java @@ -0,0 +1,145 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Declarable; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.Lifecycle; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * The {@link AbstractTestContainerTests} extension + * + * @author Artem Bilan + * + * @since 4.0 + */ +@SpringJUnitConfig +@DirtiesContext +public abstract class RabbitAmqpTestBase extends AbstractTestContainerTests { + + @Autowired + protected Environment environment; + + @Autowired + protected AmqpConnectionFactory connectionFactory; + + @Autowired + protected RabbitAmqpAdmin admin; + + @Autowired + protected RabbitAmqpTemplate template; + + @Configuration + public static class AmqpCommonConfig implements Lifecycle { + + @Autowired + List declarables; + + @Autowired(required = false) + List declarableContainers = new ArrayList<>(); + + @Autowired + RabbitAmqpAdmin admin; + + @Bean + Environment environment() { + return new AmqpEnvironmentBuilder() + .connectionSettings() + .port(amqpPort()) + .environmentBuilder() + .build(); + } + + @Bean + AmqpConnectionFactory connectionFactory(Environment environment) { + return new SingleAmqpConnectionFactory(environment); + } + + @Bean + RabbitAmqpAdmin admin(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpAdmin(connectionFactory); + } + + @Bean + RabbitAmqpTemplate rabbitTemplate(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpTemplate(connectionFactory); + } + + @Bean + TopicExchange dlx1() { + return new TopicExchange("dlx1"); + } + + @Bean + Queue dlq1() { + return new Queue("dlq1"); + } + + @Bean + Binding dlq1Binding() { + return BindingBuilder.bind(dlq1()).to(dlx1()).with("#"); + } + + volatile boolean running; + + @Override + public void start() { + this.running = true; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public void stop() { + Stream.concat(this.declarables.stream(), + this.declarableContainers.stream() + .flatMap((declarables) -> declarables.getDeclarables().stream())) + .filter((declarable) -> declarable instanceof Queue || declarable instanceof Exchange) + .forEach((declarable) -> { + if (declarable instanceof Queue queue) { + this.admin.deleteQueue(queue.getName()); + } + else { + this.admin.deleteExchange(((Exchange) declarable).getName()); + } + }); + } + + } + +} diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java new file mode 100644 index 0000000000..426f8f9097 --- /dev/null +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java @@ -0,0 +1,305 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbitmq.client.listener; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import com.rabbitmq.client.amqp.Consumer; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.amqp.core.AmqpAcknowledgment; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueBuilder; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTestBase; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Artem Bilan + * + * @since 4.0 + */ +@ContextConfiguration +class RabbitAmqpListenerTests extends RabbitAmqpTestBase { + + @Autowired + Config config; + + @Autowired + RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry; + + @Test + @SuppressWarnings("unchecked") + void verifyAllDataIsConsumedFromQ1AndQ2() throws InterruptedException { + MessageListenerContainer testAmqpListener = + this.rabbitListenerEndpointRegistry.getListenerContainer("testAmqpListener"); + + assertThat(testAmqpListener).extracting("queueToConsumers") + .asInstanceOf(InstanceOfAssertFactories.map(String.class, List.class)) + .hasSize(2) + .values() + .flatMap(list -> (List) list) + .hasSize(4); + + List testDataList = + List.of("data1", "data2", "requeue", "data4", "data5", "discard", "data7", "data8", "discard", "data10"); + + Random random = new Random(); + + for (String testData : testDataList) { + this.template.convertAndSend((random.nextInt(2) == 0 ? "q1" : "q2"), testData); + } + + assertThat(this.config.consumeIsDone.await(20, TimeUnit.SECONDS)).isTrue(); + + synchronized (this.config.received) { + assertThat(this.config.received).containsAll(testDataList); + } + + assertThat(this.template.receive("dlq1")).succeedsWithin(20, TimeUnit.SECONDS); + assertThat(this.template.receive("dlq1")).succeedsWithin(20, TimeUnit.SECONDS); + } + + @Test + @SuppressWarnings("unchecked") + void verifyBatchConsumedAfterScheduledTimeout() { + List testDataList = + List.of("batchData1", "batchData2", "batchData3", "batchData4", "batchData5"); + + for (String testData : testDataList) { + this.template.convertAndSend("q3", testData); + } + + assertThat(this.config.batchReceived).succeedsWithin(20, TimeUnit.SECONDS) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .hasSize(5) + .containsAll(testDataList); + + assertThat(this.config.batchReceivedOnThread).startsWith("batch-consumer-scheduler-"); + + MessageListenerContainer testBatchListener = + this.rabbitListenerEndpointRegistry.getListenerContainer("testBatchListener"); + + MultiValueMap queueToConsumers = + TestUtils.getPropertyValue(testBatchListener, "queueToConsumers", MultiValueMap.class); + Consumer consumer = queueToConsumers.get("q3").get(0); + + assertThat(consumer.unsettledMessageCount()).isEqualTo(0L); + + this.config.batchReceived = new CompletableFuture<>(); + + testDataList = + IntStream.range(6, 16) + .boxed() + .map(Object::toString) + .map("batchData"::concat) + .toList(); + + for (String testData : testDataList) { + this.template.convertAndSend("q3", testData); + } + + assertThat(this.config.batchReceived).succeedsWithin(20, TimeUnit.SECONDS) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .hasSize(10) + .containsAll(testDataList); + + assertThat(this.config.batchReceivedOnThread).startsWith("dispatching-rabbitmq-amqp-"); + } + + @Test + void verifyBasicRequestReply() { + CompletableFuture replyFuture = this.template.convertSendAndReceive("requestQueue", "test data"); + assertThat(replyFuture).succeedsWithin(20, TimeUnit.SECONDS).isEqualTo("TEST DATA"); + } + + @Test + void verifyFutureReturnRequestReply() { + CompletableFuture replyFuture = this.template.convertSendAndReceive("requestQueue2", "TEST DATA2"); + assertThat(replyFuture).succeedsWithin(20, TimeUnit.SECONDS).isEqualTo("test data2"); + } + + @Test + void verifyMonoReturnRequestReply() { + CompletableFuture replyFuture = this.template.convertSendAndReceive("requestQueue3", "test data3"); + assertThat(replyFuture).succeedsWithin(20, TimeUnit.SECONDS).isEqualTo("Mono test data3"); + } + + @Test + void verifyReplyOnAnotherQueue() { + this.template.convertAndSend("requestQueue4", "test data4"); + CompletableFuture replyFuture = this.template.receiveAndConvert("q4"); + assertThat(replyFuture).succeedsWithin(20, TimeUnit.SECONDS) + .isEqualTo("Reply for 'test data4' via 'e1' and 'k4'"); + } + + @Configuration + @EnableRabbit + static class Config { + + @Bean + Queue q1() { + return QueueBuilder.durable("q1").deadLetterExchange("dlx1").build(); + } + + @Bean + Queue q2() { + return QueueBuilder.durable("q2").deadLetterExchange("dlx1").build(); + } + + @Bean + Queue q3() { + return new Queue("q3"); + } + + @Bean + DirectExchange e1() { + return new DirectExchange("e1"); + } + + @Bean + Queue q4() { + return new Queue("q4"); + } + + @Bean + Binding b4() { + return BindingBuilder.bind(q4()).to(e1()).with("k4"); + } + + @Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME) + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpListenerContainerFactory(connectionFactory); + } + + final List received = Collections.synchronizedList(new ArrayList<>()); + + CountDownLatch consumeIsDone = new CountDownLatch(11); + + @RabbitListener(queues = {"q1", "q2"}, + ackMode = "#{T(org.springframework.amqp.core.AcknowledgeMode).MANUAL}", + concurrency = "2", + id = "testAmqpListener") + void processQ1AndQ2Data(String data, AmqpAcknowledgment acknowledgment, Consumer.Context context) { + try { + if ("discard".equals(data)) { + if (!this.received.contains(data)) { + context.discard(); + } + else { + throw new MessageConversionException("Test message is rejected"); + } + } + else if ("requeue".equals(data) && !this.received.contains(data)) { + acknowledgment.acknowledge(AmqpAcknowledgment.Status.REQUEUE); + } + else { + acknowledgment.acknowledge(); + } + this.received.add(data); + } + finally { + this.consumeIsDone.countDown(); + } + } + + @Bean + ThreadPoolTaskScheduler taskScheduler() { + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(2); + threadPoolTaskScheduler.setThreadNamePrefix("batch-consumer-scheduler-"); + return threadPoolTaskScheduler; + } + + @Bean + RabbitAmqpListenerContainerFactory batchRabbitAmqpListenerContainerFactory( + AmqpConnectionFactory connectionFactory, ThreadPoolTaskScheduler taskScheduler) { + + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory = + new RabbitAmqpListenerContainerFactory(connectionFactory); + rabbitAmqpListenerContainerFactory.setTaskScheduler(taskScheduler); + rabbitAmqpListenerContainerFactory.setBatchSize(10); + rabbitAmqpListenerContainerFactory.setBatchReceiveTimeout(1000L); + return rabbitAmqpListenerContainerFactory; + } + + CompletableFuture> batchReceived = new CompletableFuture<>(); + + volatile String batchReceivedOnThread; + + @RabbitListener(queues = "q3", + containerFactory = "batchRabbitAmqpListenerContainerFactory", + id = "testBatchListener") + void processBatchFromQ3(List data) { + this.batchReceivedOnThread = Thread.currentThread().getName(); + this.batchReceived.complete(data); + } + + @RabbitListener(queuesToDeclare = @org.springframework.amqp.rabbit.annotation.Queue("requestQueue")) + String toUpperCaseRpc(String data) { + return data.toUpperCase(); + } + + @RabbitListener(queuesToDeclare = @org.springframework.amqp.rabbit.annotation.Queue("requestQueue2")) + CompletableFuture toLowerCaseFutureRpc(String data) { + return CompletableFuture.completedFuture(data) + .thenApply(String::toLowerCase); + } + + @RabbitListener(queuesToDeclare = @org.springframework.amqp.rabbit.annotation.Queue("requestQueue3")) + Mono monoRpc(String data) { + return Mono.just(data) + .map(value -> "Mono " + value); + } + + @RabbitListener(queuesToDeclare = @org.springframework.amqp.rabbit.annotation.Queue("requestQueue4")) + @SendTo("e1/k4") + String replyViaSendTo(String data) { + return "Reply for '%s' via 'e1' and 'k4'".formatted(data); + } + + } + +} diff --git a/spring-rabbitmq-client/src/test/resources/log4j2-test.xml b/spring-rabbitmq-client/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000000..752714e330 --- /dev/null +++ b/spring-rabbitmq-client/src/test/resources/log4j2-test.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/ant/upload-dist.xml b/src/ant/upload-dist.xml deleted file mode 100644 index be958eb51c..0000000000 --- a/src/ant/upload-dist.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Copying dist .ZIP to ${dist.staging} - - - - - diff --git a/src/api/overview.html b/src/api/overview.html index f7208c81ab..fe4739a2cf 100644 --- a/src/api/overview.html +++ b/src/api/overview.html @@ -1,24 +1,15 @@ - This document is the API specification for Spring AMQP -
+ This document is the API specification for Spring AMQP +

- For further API reference and developer documentation, see the Spring AMQP reference documentation. That - documentation contains more detailed, developer-targeted - descriptions, with conceptual overviews, definitions of terms, - workarounds, and working code examples. + For further API reference and developer documentation, see the Spring AMQP reference documentation. + That documentation contains more detailed, developer-targeted descriptions, with conceptual overviews, definitions of terms, workarounds, and working code examples.

- If you are interested in commercial training, consultancy, and - support for Spring AMQP, please visit - https://www.spring.io + If you are interested in commercial training, consultancy, and support for Spring AMQP, please visit https://www.spring.io

diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index 92584d739d..c4acb91b06 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -13,9 +13,7 @@ - - - + @@ -89,10 +87,10 @@ - + - + @@ -100,9 +98,6 @@ - - - @@ -176,7 +171,9 @@ - + + + diff --git a/src/eclipse/org.eclipse.core.resources.prefs b/src/eclipse/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..99f26c0203 --- /dev/null +++ b/src/eclipse/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/src/eclipse/org.eclipse.jdt.core.prefs b/src/eclipse/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..3b3666c96c --- /dev/null +++ b/src/eclipse/org.eclipse.jdt.core.prefs @@ -0,0 +1,469 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding//src/test/resources=UTF-8 +org.eclipse.jdt.core.codeComplete.argumentPrefixes= +org.eclipse.jdt.core.codeComplete.argumentSuffixes= +org.eclipse.jdt.core.codeComplete.fieldPrefixes= +org.eclipse.jdt.core.codeComplete.fieldSuffixes= +org.eclipse.jdt.core.codeComplete.localPrefixes= +org.eclipse.jdt.core.codeComplete.localSuffixes= +org.eclipse.jdt.core.codeComplete.staticFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=enabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=info +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=ignore +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false +org.eclipse.jdt.core.formatter.align_with_spaces=false +org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false +org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=false +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false +org.eclipse.jdt.core.formatter.comment.indent_root_tags=false +org.eclipse.jdt.core.formatter.comment.indent_tag_description=false +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert +org.eclipse.jdt.core.formatter.comment.line_length=80 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false +org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.lineSplit=90 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=tab +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.use_on_off_tags=true +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true +org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true +org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true +org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true +org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true diff --git a/src/eclipse/org.eclipse.jdt.ui.prefs b/src/eclipse/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000000..1961fa9533 --- /dev/null +++ b/src/eclipse/org.eclipse.jdt.ui.prefs @@ -0,0 +1,66 @@ +cleanup.add_default_serial_version_id=true +cleanup.add_generated_serial_version_id=false +cleanup.add_missing_annotations=false +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=false +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_missing_override_annotations_interface_methods=true +cleanup.add_serial_version_id=false +cleanup.always_use_blocks=true +cleanup.always_use_parentheses_in_expressions=false +cleanup.always_use_this_for_non_static_field_access=true +cleanup.always_use_this_for_non_static_method_access=false +cleanup.convert_to_enhanced_for_loop=false +cleanup.correct_indentation=false +cleanup.format_source_code=false +cleanup.format_source_code_changes_only=false +cleanup.make_local_variable_final=false +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=true +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=true +cleanup.never_use_blocks=false +cleanup.never_use_parentheses_in_expressions=true +cleanup.organize_imports=false +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=true +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.remove_private_constructors=true +cleanup.remove_trailing_whitespaces=true +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=false +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=true +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=false +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=false +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.use_blocks=true +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_parentheses_in_expressions=false +cleanup.use_this_for_non_static_field_access=true +cleanup.use_this_for_non_static_field_access_only_if_necessary=false +cleanup.use_this_for_non_static_method_access=false +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup_profile=_Spring +cleanup_settings_version=2 +eclipse.preferences.version=1 +formatter_profile=_spring +formatter_settings_version=13 +org.eclipse.jdt.ui.exception.name=e +org.eclipse.jdt.ui.gettersetter.use.is=true +org.eclipse.jdt.ui.ignorelowercasenames=true +org.eclipse.jdt.ui.importorder=java;javax;;org.springframework;\#; +org.eclipse.jdt.ui.javadoc=true +org.eclipse.jdt.ui.keywordthis=false +org.eclipse.jdt.ui.ondemandthreshold=9999 +org.eclipse.jdt.ui.overrideannotation=true +org.eclipse.jdt.ui.staticondemandthreshold=9999 +org.eclipse.jdt.ui.text.custom_code_templates= diff --git a/src/idea/spring-framework.xml b/src/idea/spring-framework.xml new file mode 100644 index 0000000000..a66a6d14c3 --- /dev/null +++ b/src/idea/spring-framework.xml @@ -0,0 +1,269 @@ + + + + + \ No newline at end of file diff --git a/src/reference/antora/antora-playbook.yml b/src/reference/antora/antora-playbook.yml new file mode 100644 index 0000000000..1b86b09228 --- /dev/null +++ b/src/reference/antora/antora-playbook.yml @@ -0,0 +1,36 @@ +antora: + extensions: + - require: '@springio/antora-extensions' + root_component_name: 'amqp' + +site: + title: Spring AMQP + url: https://docs.spring.io/spring-amqp/reference/ +content: + sources: + - url: ./../../.. + branches: HEAD + # See https://docs.antora.org/antora/latest/playbook/content-source-start-path/#start-path-key + start_path: src/reference/antora + worktrees: true +asciidoc: + attributes: + page-stackoverflow-url: https://stackoverflow.com/tags/spring-amqp + page-pagination: '' + hide-uri-scheme: '@' + tabs-sync-option: '@' + chomp: 'all' + extensions: + - '@asciidoctor/tabs' + - '@springio/asciidoctor-extensions' + - '@springio/asciidoctor-extensions/javadoc-extension' + sourcemap: true +urls: + latest_version_segment: '' +runtime: + log: + failure_level: warn + format: pretty +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip diff --git a/src/reference/antora/antora.yml b/src/reference/antora/antora.yml new file mode 100644 index 0000000000..b12a23a465 --- /dev/null +++ b/src/reference/antora/antora.yml @@ -0,0 +1,21 @@ +name: amqp +version: true +title: Spring AMQP +nav: + - modules/ROOT/nav.adoc +ext: + collector: + run: + command: gradlew -q :generateAntoraResources + local: true + scan: + dir: build/generated-antora-resources + +asciidoc: + attributes: + attribute-missing: 'warn' + chomp: 'all' + # External projects URLs and related attributes + rabbitmq-stream-docs: 'https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle' + rabbitmq-github: 'https://github.com/rabbitmq' + rabbitmq-server-github: '{rabbitmq-github}/rabbitmq-server/tree/main/deps' diff --git a/src/reference/asciidoc/images/cacheStats.png b/src/reference/antora/modules/ROOT/assets/images/cacheStats.png similarity index 100% rename from src/reference/asciidoc/images/cacheStats.png rename to src/reference/antora/modules/ROOT/assets/images/cacheStats.png diff --git a/src/reference/asciidoc/images/tickmark.png b/src/reference/antora/modules/ROOT/assets/images/tickmark.png similarity index 100% rename from src/reference/asciidoc/images/tickmark.png rename to src/reference/antora/modules/ROOT/assets/images/tickmark.png diff --git a/src/reference/antora/modules/ROOT/nav.adoc b/src/reference/antora/modules/ROOT/nav.adoc new file mode 100644 index 0000000000..8bc51e5acd --- /dev/null +++ b/src/reference/antora/modules/ROOT/nav.adoc @@ -0,0 +1,85 @@ +* xref:index.adoc[] +* xref:whats-new.adoc[] +* xref:introduction/index.adoc[] +** xref:introduction/quick-tour.adoc[] +* xref:reference.adoc[] +** xref:amqp.adoc[] +*** xref:amqp/abstractions.adoc[] +*** xref:amqp/connections.adoc[] +*** xref:amqp/custom-client-props.adoc[] +*** xref:amqp/template.adoc[] +*** xref:amqp/sending-messages.adoc[] +*** xref:amqp/receiving-messages.adoc[] +**** xref:amqp/receiving-messages/polling-consumer.adoc[] +**** xref:amqp/receiving-messages/async-consumer.adoc[] +**** xref:amqp/receiving-messages/de-batching.adoc[] +**** xref:amqp/receiving-messages/consumer-events.adoc[] +**** xref:amqp/receiving-messages/consumerTags.adoc[] +**** xref:amqp/receiving-messages/async-annotation-driven.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/meta.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/enable.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/conversion.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/custom-argument-resolver.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/registration.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/enable-signature.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/rabbit-validation.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/proxy-rabbitlistener-and-generics.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/error-handling.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/container-management.adoc[] +**** xref:amqp/receiving-messages/batch.adoc[] +**** xref:amqp/receiving-messages/using-container-factories.adoc[] +**** xref:amqp/receiving-messages/async-returns.adoc[] +**** xref:amqp/receiving-messages/threading.adoc[] +**** xref:amqp/receiving-messages/choose-container.adoc[] +**** xref:amqp/receiving-messages/idle-containers.adoc[] +**** xref:amqp/receiving-messages/micrometer.adoc[] +**** xref:amqp/receiving-messages/micrometer-observation.adoc[] +*** xref:amqp/containers-and-broker-named-queues.adoc[] +*** xref:amqp/message-converters.adoc[] +*** xref:amqp/post-processing.adoc[] +*** xref:amqp/request-reply.adoc[] +*** xref:amqp/broker-configuration.adoc[] +*** xref:amqp/broker-events.adoc[] +*** xref:amqp/delayed-message-exchange.adoc[] +*** xref:amqp/management-rest-api.adoc[] +*** xref:amqp/exception-handling.adoc[] +*** xref:amqp/transactions.adoc[] +*** xref:amqp/containerAttributes.adoc[] +*** xref:amqp/listener-concurrency.adoc[] +*** xref:amqp/exclusive-consumer.adoc[] +*** xref:amqp/listener-queues.adoc[] +*** xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc[] +*** xref:amqp/multi-rabbit.adoc[] +*** xref:amqp/debugging.adoc[] +** xref:stream.adoc[] +** xref:rabbitmq-amqp-client.adoc[] +** xref:logging.adoc[] +** xref:sample-apps.adoc[] +** xref:testing.adoc[] +* xref:integration-reference.adoc[] +* xref:resources.adoc[] +** xref:further-reading.adoc[] +* xref:appendix/micrometer.adoc[] +* xref:appendix/native.adoc[] +* Change History +** xref:appendix/current-release.adoc[] +** xref:appendix/previous-whats-new.adoc[] +*** xref:appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc[] +*** xref:appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc[] +*** xref:appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc[] +*** xref:appendix/previous-whats-new/changes-to-1-1-since-1-0.adoc[] diff --git a/src/reference/antora/modules/ROOT/pages/amqp.adoc b/src/reference/antora/modules/ROOT/pages/amqp.adoc new file mode 100644 index 0000000000..79cba0ebaf --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp.adoc @@ -0,0 +1,6 @@ +[[amqp]] += Using Spring AMQP +:page-section-summary-toc: 1 + +This chapter explores the interfaces and classes that are the essential components for developing applications with Spring AMQP. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc new file mode 100644 index 0000000000..9d7bad429f --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc @@ -0,0 +1,189 @@ +[[amqp-abstractions]] += AMQP Abstractions + +Spring AMQP consists of two modules (each represented by a JAR in the distribution): `spring-amqp` and `spring-rabbit`. +The 'spring-amqp' module contains the `org.springframework.amqp.core` package. +Within that package, you can find the classes that represent the core AMQP "`model`". +Our intention is to provide generic abstractions that do not rely on any particular AMQP broker implementation or client library. +End user code can be more portable across vendor implementations as it can be developed against the abstraction layer only. +These abstractions are then implemented by broker-specific modules, such as 'spring-rabbit'. +There is currently only a RabbitMQ implementation. +However, the abstractions have been validated in .NET using Apache Qpid in addition to RabbitMQ. +Since AMQP operates at the protocol level, in principle, you can use the RabbitMQ client with any broker that supports the same protocol version, but we do not test any other brokers at present. + +This overview assumes that you are already familiar with the basics of the AMQP specification. +If not, have a look at the resources listed in xref:index.adoc#resources[Other Resources] + +[[message]] +== `Message` + +The 0-9-1 AMQP specification does not define a `Message` class or interface. +Instead, when performing an operation such as `basicPublish()`, the content is passed as a byte-array argument and additional properties are passed in as separate arguments. +Spring AMQP defines a `Message` class as part of a more general AMQP domain model representation. +The purpose of the `Message` class is to encapsulate the body and properties within a single instance so that the API can, in turn, be simpler. +The following example shows the `Message` class definition: + +[source,java] +---- +public class Message { + + private final MessageProperties messageProperties; + + private final byte[] body; + + public Message(byte[] body, MessageProperties messageProperties) { + this.body = body; + this.messageProperties = messageProperties; + } + + public byte[] getBody() { + return this.body; + } + + public MessageProperties getMessageProperties() { + return this.messageProperties; + } +} +---- + +The `MessageProperties` interface defines several common properties, such as 'messageId', 'timestamp', 'contentType', and several more. +You can also extend those properties with user-defined 'headers' by calling the `setHeader(String key, Object value)` method. + +IMPORTANT: Starting with versions `1.5.7`, `1.6.11`, `1.7.4`, and `2.0.0`, if a message body is a serialized `Serializable` java object, it is no longer deserialized (by default) when performing `toString()` operations (such as in log messages). +This is to prevent unsafe deserialization. +By default, only `java.util` and `java.lang` classes are deserialized. +To revert to the previous behavior, you can add allowable class/package patterns by invoking `Message.addAllowedListPatterns(...)`. +A simple `\*` wildcard is supported, for example `com.something.*, *.MyClass`. +Bodies that cannot be deserialized are represented by `byte[]` in log messages. + +[[exchange]] +== Exchange + +The `Exchange` interface represents an AMQP Exchange, which is what a Message Producer sends to. +Each Exchange within a virtual host of a broker has a unique name as well as a few other properties. +The following example shows the `Exchange` interface: + +[source,java] +---- +public interface Exchange { + + String getName(); + + String getExchangeType(); + + boolean isDurable(); + + boolean isAutoDelete(); + + Map getArguments(); + +} +---- + +As you can see, an `Exchange` also has a 'type' represented by constants defined in `ExchangeTypes`. +The basic types are: `direct`, `topic`, `fanout`, and `headers`. +In the core package, you can find implementations of the `Exchange` interface for each of those types. +The behavior varies across these `Exchange` types in terms of how they handle bindings to queues. +For example, a `Direct` exchange lets a queue be bound by a fixed routing key (often the queue's name). +A `Topic` exchange supports bindings with routing patterns that may include the '*' and '#' wildcards for 'exactly-one' and 'zero-or-more', respectively. +The `Fanout` exchange publishes to all queues that are bound to it without taking any routing key into consideration. +For much more information about these and the other Exchange types, see https://www.rabbitmq.com/tutorials/amqp-concepts#exchanges[AMQP Exchanges]. + +Starting with version 3.2, the `ConsistentHashExchange` type has been introduced for convenience during application configuration phase. +It provided options like `x-consistent-hash` for an exchange type. +Allows to configure `hash-header` or `hash-property` exchange definition argument. +The respective RabbitMQ `rabbitmq_consistent_hash_exchange` plugin has to be enabled on the broker. +More information about the purpose, logic and behavior of the Consistent Hash Exchange are in the official RabbitMQ {rabbitmq-server-github}/rabbitmq_consistent_hash_exchange[documentation]. + +NOTE: The AMQP specification also requires that any broker provide a "`default`" direct exchange that has no name. +All queues that are declared are bound to that default `Exchange` with their names as routing keys. +You can learn more about the default Exchange's usage within Spring AMQP in xref:amqp/template.adoc[`AmqpTemplate`]. + +[[queue]] +== Queue + +The `Queue` class represents the component from which a message consumer receives messages. +Like the various `Exchange` classes, our implementation is intended to be an abstract representation of this core AMQP type. +The following listing shows the `Queue` class: + +[source,java] +---- +public class Queue { + + private final String name; + + private volatile boolean durable; + + private volatile boolean exclusive; + + private volatile boolean autoDelete; + + private volatile Map arguments; + + /** + * The queue is durable, non-exclusive and non auto-delete. + * + * @param name the name of the queue. + */ + public Queue(String name) { + this(name, true, false, false); + } + + // Getters and Setters omitted for brevity + +} +---- + +Notice that the constructor takes the queue name. +Depending on the implementation, the admin template may provide methods for generating a uniquely named queue. +Such queues can be useful as a "`reply-to`" address or in other *temporary* situations. +For that reason, the 'exclusive' and 'autoDelete' properties of an auto-generated queue would both be set to 'true'. + +NOTE: See the section on queues in xref:amqp/broker-configuration.adoc[Configuring the Broker] for information about declaring queues by using namespace support, including queue arguments. + +[[binding]] +== Binding + +Given that a producer sends to an exchange and a consumer receives from a queue, the bindings that connect queues to exchanges are critical for connecting those producers and consumers via messaging. +In Spring AMQP, we define a `Binding` class to represent those connections. +This section reviews the basic options for binding queues to exchanges. + +You can bind a queue to a `DirectExchange` with a fixed routing key, as the following example shows: + +[source,java] +---- +new Binding(someQueue, someDirectExchange, "foo.bar"); +---- + +You can bind a queue to a `TopicExchange` with a routing pattern, as the following example shows: + +[source,java] +---- +new Binding(someQueue, someTopicExchange, "foo.*"); +---- + +You can bind a queue to a `FanoutExchange` with no routing key, as the following example shows: + +[source,java] +---- +new Binding(someQueue, someFanoutExchange); +---- + +We also provide a `BindingBuilder` to facilitate a "`fluent API`" style, as the following example shows: + +[source,java] +---- +Binding b = BindingBuilder.bind(someQueue).to(someTopicExchange).with("foo.*"); +---- + +NOTE: For clarity, the preceding example shows the `BindingBuilder` class, but this style works well when using a static import for the 'bind()' method. + +By itself, an instance of the `Binding` class only holds the data about a connection. +In other words, it is not an "`active`" component. +However, as you will see later in xref:amqp/broker-configuration.adoc[Configuring the Broker], the `AmqpAdmin` class can use `Binding` instances to actually trigger the binding actions on the broker. +Also, as you can see in that same section, you can define the `Binding` instances by using Spring's `@Bean` annotations within `@Configuration` classes. +There is also a convenient base class that further simplifies that approach for generating AMQP-related bean definitions and recognizes the queues, exchanges, and bindings so that they are all declared on the AMQP broker upon application startup. + +The `AmqpTemplate` is also defined within the core package. +As one of the main components involved in actual AMQP messaging, it is discussed in detail in its own section (see xref:amqp/template.adoc[`AmqpTemplate`]). + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc new file mode 100644 index 0000000000..20377cbd6b --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc @@ -0,0 +1,684 @@ +[[broker-configuration]] += Configuring the Broker + +The AMQP specification describes how the protocol can be used to configure queues, exchanges, and bindings on the broker. +These operations (which are portable from the 0.8 specification and higher) are present in the `AmqpAdmin` interface in the `org.springframework.amqp.core` package. +The RabbitMQ implementation of that class is `RabbitAdmin` located in the `org.springframework.amqp.rabbit.core` package. + +The `AmqpAdmin` interface is based on using the Spring AMQP domain abstractions and is shown in the following listing: + +[source,java] +---- +public interface AmqpAdmin { + + // Exchange Operations + + void declareExchange(Exchange exchange); + + void deleteExchange(String exchangeName); + + // Queue Operations + + Queue declareQueue(); + + String declareQueue(Queue queue); + + void deleteQueue(String queueName); + + void deleteQueue(String queueName, boolean unused, boolean empty); + + void purgeQueue(String queueName, boolean noWait); + + // Binding Operations + + void declareBinding(Binding binding); + + void removeBinding(Binding binding); + + Properties getQueueProperties(String queueName); + + QueueInformation getQueueInfo(String queueName); + +} +---- + +See also xref:amqp/template.adoc#scoped-operations[Scoped Operations]. + +The `getQueueProperties()` method returns some limited information about the queue (message count and consumer count). +The keys for the properties returned are available as constants in the `RabbitAdmin` (`QUEUE_NAME`, +`QUEUE_MESSAGE_COUNT`, and `QUEUE_CONSUMER_COUNT`). +The `getQueueInfo()` returns a convenient `QueueInformation` data object. + +The no-arg `declareQueue()` method defines a queue on the broker with a name that is automatically generated. +The additional properties of this auto-generated queue are `exclusive=true`, `autoDelete=true`, and `durable=false`. + +The `declareQueue(Queue queue)` method takes a `Queue` object and returns the name of the declared queue. +If the `name` property of the provided `Queue` is an empty `String`, the broker declares the queue with a generated name. +That name is returned to the caller. +That name is also added to the `actualName` property of the `Queue`. +You can use this functionality programmatically only by invoking the `RabbitAdmin` directly. +When using auto-declaration by the admin when defining a queue declaratively in the application context, you can set the name property to `""` (the empty string). +The broker then creates the name. +Starting with version 2.1, listener containers can use queues of this type. +See xref:amqp/containers-and-broker-named-queues.adoc[Containers and Broker-Named queues] for more information. + +This is in contrast to an `AnonymousQueue` where the framework generates a unique (`UUID`) name and sets `durable` to +`false` and `exclusive`, `autoDelete` to `true`. +A `` with an empty (or missing) `name` attribute always creates an `AnonymousQueue`. + +See xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`] to understand why `AnonymousQueue` is preferred over broker-generated queue names as well as +how to control the format of the name. +Starting with version 2.1, anonymous queues are declared with argument `Queue.X_QUEUE_LEADER_LOCATOR` set to `client-local` by default. +This ensures that the queue is declared on the node to which the application is connected. +Declarative queues must have fixed names because they might be referenced elsewhere in the context -- such as in the +listener shown in the following example: + +[source,xml] +---- + + + +---- + +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#automatic-declaration[Automatic Declaration of Exchanges, Queues, and Bindings]. + +The RabbitMQ implementation of this interface is `RabbitAdmin`, which, when configured by using Spring XML, resembles the following example: + +[source,xml] +---- + + + +---- + +When the `CachingConnectionFactory` cache mode is `CHANNEL` (the default), the `RabbitAdmin` implementation does automatic lazy declaration of queues, exchanges, and bindings declared in the same `ApplicationContext`. +These components are declared as soon as a `Connection` is opened to the broker. +There are some namespace features that make this very convenient -- for example, +in the Stocks sample application, we have the following: + +[source,xml] +---- + + + + + + + + + + + + + + + +---- + +In the preceding example, we use anonymous queues (actually, internally, just queues with names generated by the framework, not by the broker) and refer to them by ID. +We can also declare queues with explicit names, which also serve as identifiers for their bean definitions in the context. +The following example configures a queue with an explicit name: + +[source,xml] +---- + +---- + +TIP: You can provide both `id` and `name` attributes. +This lets you refer to the queue (for example, in a binding) by an ID that is independent of the queue name. +It also allows standard Spring features (such as property placeholders and SpEL expressions for the queue name). +These features are not available when you use the name as the bean identifier. + +Queues can be configured with additional arguments -- for example, `x-message-ttl`. +When you use the namespace support, they are provided in the form of a `Map` of argument-name/argument-value pairs, which are defined by using the `` element. +The following example shows how to do so: + +[source,xml] +---- + + + + + + +---- + +By default, the arguments are assumed to be strings. +For arguments of other types, you must provide the type. +The following example shows how to specify the type: + +[source,xml] +---- + + + + + +---- + +When providing arguments of mixed types, you must provide the type for each entry element. +The following example shows how to do so: + +[source,xml] +---- + + + + 100 + + + + + +---- + +With Spring Framework 3.2 and later, this can be declared a little more succinctly, as follows: + +[source,xml] +---- + + + + + + +---- + +When you use Java configuration, the `Queue.X_QUEUE_LEADER_LOCATOR` argument is supported as a first class property through the `setLeaderLocator()` method on the `Queue` class. +Starting with version 2.1, anonymous queues are declared with this property set to `client-local` by default. +This ensures that the queue is declared on the node the application is connected to. + +IMPORTANT: The RabbitMQ broker does not allow declaration of a queue with mismatched arguments. +For example, if a `queue` already exists with no `time to live` argument, and you attempt to declare it with (for example) `key="x-message-ttl" value="100"`, an exception is thrown. + +By default, the `RabbitAdmin` immediately stops processing all declarations when any exception occurs. +This could cause downstream issues, such as a listener container failing to initialize because another queue (defined after the one in error) is not declared. + +This behavior can be modified by setting the `ignore-declaration-exceptions` attribute to `true` on the `RabbitAdmin` instance. +This option instructs the `RabbitAdmin` to log the exception and continue declaring other elements. +When configuring the `RabbitAdmin` using Java, this property is called `ignoreDeclarationExceptions`. +This is a global setting that applies to all elements. +Queues, exchanges, and bindings have a similar property that applies to just those elements. + +Prior to version 1.6, this property took effect only if an `IOException` occurred on the channel, such as when there is a mismatch between current and desired properties. +Now, this property takes effect on any exception, including `TimeoutException` and others. + +In addition, any declaration exceptions result in the publishing of a `DeclarationExceptionEvent`, which is an `ApplicationEvent` that can be consumed by any `ApplicationListener` in the context. +The event contains a reference to the admin, the element that was being declared, and the `Throwable`. + +[[headers-exchange]] +== Headers Exchange + +Starting with version 1.3, you can configure the `HeadersExchange` to match on multiple headers. +You can also specify whether any or all headers must match. +The following example shows how to do so: + +[source,xml] +---- + + + + + + + + + + + +---- + +Starting with version 1.6, you can configure `Exchanges` with an `internal` flag (defaults to `false`) and such an +`Exchange` is properly configured on the Broker through a `RabbitAdmin` (if one is present in the application context). +If the `internal` flag is `true` for an exchange, RabbitMQ does not let clients use the exchange. +This is useful for a dead letter exchange or exchange-to-exchange binding, where you do not wish the exchange to be used +directly by publishers. + +To see how to use Java to configure the AMQP infrastructure, look at the Stock sample application, +where there is the `@Configuration` class `AbstractStockRabbitConfiguration`, which ,in turn has +`RabbitClientConfiguration` and `RabbitServerConfiguration` subclasses. +The following listing shows the code for `AbstractStockRabbitConfiguration`: + +[source,java] +---- +@Configuration +public abstract class AbstractStockAppRabbitConfiguration { + + @Bean + public CachingConnectionFactory connectionFactory() { + CachingConnectionFactory connectionFactory = + new CachingConnectionFactory("localhost"); + connectionFactory.setUsername("guest"); + connectionFactory.setPassword("guest"); + return connectionFactory; + } + + @Bean + public RabbitTemplate rabbitTemplate() { + RabbitTemplate template = new RabbitTemplate(connectionFactory()); + template.setMessageConverter(jsonMessageConverter()); + configureRabbitTemplate(template); + return template; + } + + @Bean + public Jackson2JsonMessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); + } + + @Bean + public TopicExchange marketDataExchange() { + return new TopicExchange("app.stock.marketdata"); + } + + // additional code omitted for brevity + +} +---- + +In the Stock application, the server is configured by using the following `@Configuration` class: + +[source,java] +---- +@Configuration +public class RabbitServerConfiguration extends AbstractStockAppRabbitConfiguration { + + @Bean + public Queue stockRequestQueue() { + return new Queue("app.stock.request"); + } +} +---- + +This is the end of the whole inheritance chain of `@Configuration` classes. +The end result is that `TopicExchange` and `Queue` are declared to the broker upon application startup. +There is no binding of `TopicExchange` to a queue in the server configuration, as that is done in the client application. +The stock request queue, however, is automatically bound to the AMQP default exchange. +This behavior is defined by the specification. + +The client `@Configuration` class is a little more interesting. +Its declaration follows: + +[source,java] +---- +@Configuration +public class RabbitClientConfiguration extends AbstractStockAppRabbitConfiguration { + + @Value("${stocks.quote.pattern}") + private String marketDataRoutingKey; + + @Bean + public Queue marketDataQueue() { + return amqpAdmin().declareQueue(); + } + + /** + * Binds to the market data exchange. + * Interested in any stock quotes + * that match its routing key. + */ + @Bean + public Binding marketDataBinding() { + return BindingBuilder.bind( + marketDataQueue()).to(marketDataExchange()).with(marketDataRoutingKey); + } + + // additional code omitted for brevity + +} +---- + +The client declares another queue through the `declareQueue()` method on the `AmqpAdmin`. +It binds that queue to the market data exchange with a routing pattern that is externalized in a properties file. + + +[[builder-api]] +== Builder API for Queues and Exchanges + +Version 1.6 introduces a convenient fluent API for configuring `Queue` and `Exchange` objects when using Java configuration. +The following example shows how to use it: + +[source, java] +---- +@Bean +public Queue queue() { + return QueueBuilder.nonDurable("foo") + .autoDelete() + .exclusive() + .withArgument("foo", "bar") + .build(); +} + +@Bean +public Exchange exchange() { + return ExchangeBuilder.directExchange("foo") + .autoDelete() + .internal() + .withArgument("foo", "bar") + .build(); +} +---- + +See the Javadoc for javadoc:org.springframework.amqp.core.QueueBuilder[`org.springframework.amqp.core.QueueBuilder`] and javadoc:org.springframework.amqp.core.ExchangeBuilder[`org.springframework.amqp.core.ExchangeBuilder`] for more information. + +Starting with version 2.0, the `ExchangeBuilder` now creates durable exchanges by default, to be consistent with the simple constructors on the individual `AbstractExchange` classes. +To make a non-durable exchange with the builder, use `.durable(false)` before invoking `.build()`. +The `durable()` method with no parameter is no longer provided. + +Version 2.2 introduced fluent APIs to add "well known" exchange and queue arguments... + +[source, java] +---- +@Bean +public Queue allArgs1() { + return QueueBuilder.nonDurable("all.args.1") + .ttl(1000) + .expires(200_000) + .maxLength(42) + .maxLengthBytes(10_000) + .overflow(Overflow.rejectPublish) + .deadLetterExchange("dlx") + .deadLetterRoutingKey("dlrk") + .maxPriority(4) + .lazy() + .leaderLocator(LeaderLocator.minLeaders) + .singleActiveConsumer() + .build(); +} + +@Bean +public DirectExchange ex() { + return ExchangeBuilder.directExchange("ex.with.alternate") + .durable(true) + .alternate("alternate") + .build(); +} +---- + +[[collection-declaration]] +== Declaring Collections of Exchanges, Queues, and Bindings + +You can wrap collections of `Declarable` objects (`Queue`, `Exchange`, and `Binding`) in `Declarables` objects. +The `RabbitAdmin` detects such beans (as well as discrete `Declarable` beans) in the application context, and declares the contained objects on the broker whenever a connection is established (initially and after a connection failure). +The following example shows how to do so: + +[source, java] +---- +@Configuration +public static class Config { + + @Bean + public CachingConnectionFactory cf() { + return new CachingConnectionFactory("localhost"); + } + + @Bean + public RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + @Bean + public DirectExchange e1() { + return new DirectExchange("e1", false, true); + } + + @Bean + public Queue q1() { + return new Queue("q1", false, false, true); + } + + @Bean + public Binding b1() { + return BindingBuilder.bind(q1()).to(e1()).with("k1"); + } + + @Bean + public Declarables es() { + return new Declarables( + new DirectExchange("e2", false, true), + new DirectExchange("e3", false, true)); + } + + @Bean + public Declarables qs() { + return new Declarables( + new Queue("q2", false, false, true), + new Queue("q3", false, false, true)); + } + + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public Declarables prototypes() { + return new Declarables(new Queue(this.prototypeQueueName, false, false, true)); + } + + @Bean + public Declarables bs() { + return new Declarables( + new Binding("q2", DestinationType.QUEUE, "e2", "k2", null), + new Binding("q3", DestinationType.QUEUE, "e3", "k3", null)); + } + + @Bean + public Declarables ds() { + return new Declarables( + new DirectExchange("e4", false, true), + new Queue("q4", false, false, true), + new Binding("q4", DestinationType.QUEUE, "e4", "k4", null)); + } + +} +---- + +IMPORTANT: In versions prior to 2.1, you could declare multiple `Declarable` instances by defining beans of type `Collection`. +This can cause undesirable side effects in some cases, because the admin has to iterate over all `Collection` beans. + +Version 2.2 added the `getDeclarablesByType` method to `Declarables`; this can be used as a convenience, for example, when declaring the listener container bean(s). + +[source, java] +---- +public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, + Declarables mixedDeclarables, MessageListener listener) { + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setQueues(mixedDeclarables.getDeclarablesByType(Queue.class).toArray(new Queue[0])); + container.setMessageListener(listener); + return container; +} +---- + +[[conditional-declaration]] +== Conditional Declaration + +By default, all queues, exchanges, and bindings are declared by all `RabbitAdmin` instances (assuming they have `auto-startup="true"`) in the application context. + +Starting with version 2.1.9, the `RabbitAdmin` has a new property `explicitDeclarationsOnly` (which is `false` by default); when this is set to `true`, the admin will only declare beans that are explicitly configured to be declared by that admin. + +NOTE: Starting with the 1.2 release, you can conditionally declare these elements. +This is particularly useful when an application connects to multiple brokers and needs to specify with which brokers a particular element should be declared. + +The classes representing these elements implement `Declarable`, which has two methods: `shouldDeclare()` and `getDeclaringAdmins()`. +The `RabbitAdmin` uses these methods to determine whether a particular instance should actually process the declarations on its `Connection`. + +The properties are available as attributes in the namespace, as shown in the following examples: + +[source,xml] +---- + + + + + + + + + + + + + + + + + + + +---- + +NOTE: By default, the `auto-declare` attribute is `true` and, if the `declared-by` is not supplied (or is empty), then all `RabbitAdmin` instances declare the object (as long as the admin's `auto-startup` attribute is `true`, the default, and the admin's `explicit-declarations-only` attribute is false). + +Similarly, you can use Java-based `@Configuration` to achieve the same effect. +In the following example, the components are declared by `admin1` but not by `admin2`: + +[source,java] +---- +@Bean +public RabbitAdmin admin1() { + return new RabbitAdmin(cf1()); +} + +@Bean +public RabbitAdmin admin2() { + return new RabbitAdmin(cf2()); +} + +@Bean +public Queue queue() { + Queue queue = new Queue("foo"); + queue.setAdminsThatShouldDeclare(admin1()); + return queue; +} + +@Bean +public Exchange exchange() { + DirectExchange exchange = new DirectExchange("bar"); + exchange.setAdminsThatShouldDeclare(admin1()); + return exchange; +} + +@Bean +public Binding binding() { + Binding binding = new Binding("foo", DestinationType.QUEUE, exchange().getName(), "foo", null); + binding.setAdminsThatShouldDeclare(admin1()); + return binding; +} +---- + +[[note-id-name]] +== A Note On the `id` and `name` Attributes + +The `name` attribute on `` and `` elements reflects the name of the entity in the broker. +For queues, if the `name` is omitted, an anonymous queue is created (see xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`]). + +In versions prior to 2.0, the `name` was also registered as a bean name alias (similar to `name` on `` elements). + +This caused two problems: + +* It prevented the declaration of a queue and exchange with the same name. +* The alias was not resolved if it contained a SpEL expression (`#{...}`). + +Starting with version 2.0, if you declare one of these elements with both an `id` _and_ a `name` attribute, the name is no longer declared as a bean name alias. +If you wish to declare a queue and exchange with the same `name`, you must provide an `id`. + +There is no change if the element has only a `name` attribute. +The bean can still be referenced by the `name` -- for example, in binding declarations. +However, you still cannot reference it if the name contains SpEL -- you must provide an `id` for reference purposes. + + +[[anonymous-queue]] +== `AnonymousQueue` + +In general, when you need a uniquely-named, exclusive, auto-delete queue, we recommend that you use the `AnonymousQueue` +instead of broker-defined queue names (using `""` as a `Queue` name causes the broker to generate the queue +name). + +This is because: + +. The queues are actually declared when the connection to the broker is established. +This is long after the beans are created and wired together. +Beans that use the queue need to know its name. +In fact, the broker might not even be running when the application is started. +. If the connection to the broker is lost for some reason, the admin re-declares the `AnonymousQueue` with the same name. +If we used broker-declared queues, the queue name would change. + +You can control the format of the queue name used by `AnonymousQueue` instances. + +By default, the queue name is prefixed by `spring.gen-` followed by a base64 representation of the `UUID` -- for example: `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. + +You can provide an `AnonymousQueue.NamingStrategy` implementation in a constructor argument. +The following example shows how to do so: + +[source, java] +---- +@Bean +public Queue anon1() { + return new AnonymousQueue(); +} + +@Bean +public Queue anon2() { + return new AnonymousQueue(new AnonymousQueue.Base64UrlNamingStrategy("something-")); +} + +@Bean +public Queue anon3() { + return new AnonymousQueue(AnonymousQueue.UUIDNamingStrategy.DEFAULT); +} +---- + +The first bean generates a queue name prefixed by `spring.gen-` followed by a base64 representation of the `UUID` -- for +example: `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. +The second bean generates a queue name prefixed by `something-` followed by a base64 representation of the `UUID`. +The third bean generates a name by using only the UUID (no base64 conversion) -- for example, `f20c818a-006b-4416-bf91-643590fedb0e`. + +The base64 encoding uses the "`URL and Filename Safe Alphabet`" from RFC 4648. +Trailing padding characters (`=`) are removed. + +You can provide your own naming strategy, whereby you can include other information (such as the application name or client host) in the queue name. + +You can specify the naming strategy when you use XML configuration. +The `naming-strategy` attribute is present on the `` element +for a bean reference that implements `AnonymousQueue.NamingStrategy`. +The following examples show how to specify the naming strategy in various ways: + +[source, xml] +---- + + + + + + + + + + + +---- + +The first example creates names such as `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. +The second example creates names with a String representation of a UUID. +The third example creates names such as `custom.gen-MRBv9sqISkuCiPfOYfpo4g`. + +You can also provide your own naming strategy bean. + +Starting with version 2.1, anonymous queues are declared with argument `Queue.X_QUEUE_LEADER_LOCATOR` set to `client-local` by default. +This ensures that the queue is declared on the node to which the application is connected. +You can revert to the previous behavior by calling `queue.setLeaderLocator(null)` after constructing the instance. + +[[declarable-recovery]] +== Recovering Auto-Delete Declarations + +Normally, the `RabbitAdmin` (s) only recover queues/exchanges/bindings that are declared as beans in the application context; if any such declarations are auto-delete, they will be removed by the broker if the connection is lost. +When the connection is re-established, the admin will redeclare the entities. +Normally, entities created by calling `admin.declareQueue(...)`, `admin.declareExchange(...)` and `admin.declareBinding(...)` will not be recovered. + +Starting with version 2.4, the admin has a new property `redeclareManualDeclarations`; when `true`, the admin will recover these entities in addition to the beans in the application context. + +Recovery of individual declarations will not be performed if `deleteQueue(...)`, `deleteExchange(...)` or `removeBinding(...)` is called. +Associated bindings are removed from the recoverable entities when queues and exchanges are deleted. + +Finally, calling `resetAllManualDeclarations()` will prevent the recovery of any previously declared entities. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-events.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-events.adoc new file mode 100644 index 0000000000..ab9ad77b8d --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-events.adoc @@ -0,0 +1,26 @@ +[[broker-events]] += Broker Event Listener + +When the https://www.rabbitmq.com/event-exchange.html[Event Exchange Plugin] is enabled, if you add a bean of type `BrokerEventListener` to the application context, it publishes selected broker events as `BrokerEvent` instances, which can be consumed with a normal Spring `ApplicationListener` or `@EventListener` method. +Events are published by the broker to a topic exchange `amq.rabbitmq.event` with a different routing key for each event type. +The listener uses event keys, which are used to bind an `AnonymousQueue` to the exchange so the listener receives only selected events. +Since it is a topic exchange, wildcards can be used (as well as explicitly requesting specific events), as the following example shows: + +[source, java] +---- +@Bean +public BrokerEventListener eventListener() { + return new BrokerEventListener(connectionFactory(), "user.deleted", "channel.#", "queue.#"); +} +---- + +You can further narrow the received events in individual event listeners, by using normal Spring techniques, as the following example shows: + +[source, java] +---- +@EventListener(condition = "event.eventType == 'queue.created'") +public void listener(BrokerEvent event) { + ... +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc new file mode 100644 index 0000000000..f85b670af9 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc @@ -0,0 +1,842 @@ +[[connections]] += Connection and Resource Management + +Whereas the AMQP model we described in the previous section is generic and applicable to all implementations, when we get into the management of resources, the details are specific to the broker implementation. +Therefore, in this section, we focus on code that exists only within our "`spring-rabbit`" module since, at this point, RabbitMQ is the only supported implementation. + +The central component for managing a connection to the RabbitMQ broker is the `ConnectionFactory` interface. +The responsibility of a `ConnectionFactory` implementation is to provide an instance of `org.springframework.amqp.rabbit.connection.Connection`, which is a wrapper for `com.rabbitmq.client.Connection`. + +[[choosing-factory]] +== Choosing a Connection Factory + +There are three connection factories to chose from + +* `PooledChannelConnectionFactory` +* `ThreadChannelConnectionFactory` +* `CachingConnectionFactory` + +The first two were added in version 2.3. + +For most use cases, the `CachingConnectionFactory` should be used. +The `ThreadChannelConnectionFactory` can be used if you want to ensure strict message ordering without the need to use xref:amqp/template.adoc#scoped-operations[Scoped Operations]. +The `PooledChannelConnectionFactory` is similar to the `CachingConnectionFactory` in that it uses a single connection and a pool of channels. +It's implementation is simpler but it doesn't support correlated publisher confirmations. + +Simple publisher confirmations are supported by all three factories. + +When configuring a `RabbitTemplate` to use a xref:amqp/template.adoc#separate-connection[separate connection], you can now, starting with version 2.3.2, configure the publishing connection factory to be a different type. +By default, the publishing factory is the same type and any properties set on the main factory are also propagated to the publishing factory. + +Starting with version 3.1, the `AbstractConnectionFactory` includes the `connectionCreatingBackOff` property, which supports a backoff policy in the connection module. +Currently, there is support in the behavior of `createChannel()` to handle exceptions that occur when the `channelMax` limit is reached, implementing a backoff strategy based on attempts and intervals. + +[[pooledchannelconnectionfactory]] +=== `PooledChannelConnectionFactory` + +This factory manages a single connection and two pools of channels, based on the Apache Pool2. +One pool is for transactional channels, the other is for non-transactional channels. +The pools are `GenericObjectPool` s with default configuration; a callback is provided to configure the pools; refer to the Apache documentation for more information. + +The Apache `commons-pool2` jar must be on the class path to use this factory. + +[source, java] +---- +@Bean +PooledChannelConnectionFactory pcf() throws Exception { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(rabbitConnectionFactory); + pcf.setPoolConfigurer((pool, tx) -> { + if (tx) { + // configure the transactional pool + } + else { + // configure the non-transactional pool + } + }); + return pcf; +} +---- + +[[threadchannelconnectionfactory]] +=== `ThreadChannelConnectionFactory` + +This factory manages a single connection and two `ThreadLocal` s, one for transactional channels, the other for non-transactional channels. +This factory ensures that all operations on the same thread use the same channel (as long as it remains open). +This facilitates strict message ordering without the need for xref:amqp/template.adoc#scoped-operations[Scoped Operations]. +To avoid memory leaks, if your application uses many short-lived threads, you must call the factory's `closeThreadChannel()` to release the channel resource. +Starting with version 2.3.7, a thread can transfer its channel(s) to another thread. +See xref:amqp/template.adoc#multi-strict[Strict Message Ordering in a Multi-Threaded Environment] for more information. + +[[cachingconnectionfactory]] +=== `CachingConnectionFactory` + +The third implementation provided is the `CachingConnectionFactory`, which, by default, establishes a single connection proxy that can be shared by the application. +Sharing of the connection is possible since the "`unit of work`" for messaging with AMQP is actually a "`channel`" (in some ways, this is similar to the relationship between a connection and a session in JMS). +The connection instance provides a `createChannel` method. +The `CachingConnectionFactory` implementation supports caching of those channels, and it maintains separate caches for channels based on whether they are transactional. +When creating an instance of `CachingConnectionFactory`, you can provide the 'hostname' through the constructor. +You should also provide the 'username' and 'password' properties. +To configure the size of the channel cache (the default is 25), you can call the +`setChannelCacheSize()` method. + +Starting with version 1.3, you can configure the `CachingConnectionFactory` to cache connections as well as only channels. +In this case, each call to `createConnection()` creates a new connection (or retrieves an idle one from the cache). +Closing a connection returns it to the cache (if the cache size has not been reached). +Channels created on such connections are also cached. +The use of separate connections might be useful in some environments, such as consuming from an HA cluster, in +conjunction with a load balancer, to connect to different cluster members, and others. +To cache connections, set the `cacheMode` to `CacheMode.CONNECTION`. + +NOTE: This does not limit the number of connections. +Rather, it specifies how many idle open connections are allowed. + +Starting with version 1.5.5, a new property called `connectionLimit` is provided. +When this property is set, it limits the total number of connections allowed. +When set, if the limit is reached, the `channelCheckoutTimeLimit` is used to wait for a connection to become idle. +If the time is exceeded, an `AmqpTimeoutException` is thrown. + +[IMPORTANT] +====== +When the cache mode is `CONNECTION`, automatic declaration of queues and others +(See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#automatic-declaration[Automatic Declaration of Exchanges, Queues, and Bindings]) is NOT supported. + +Also, at the time of this writing, the `amqp-client` library by default creates a fixed thread pool for each connection (default size: `Runtime.getRuntime().availableProcessors() * 2` threads). +When using a large number of connections, you should consider setting a custom `executor` on the `CachingConnectionFactory`. +Then, the same executor can be used by all connections and its threads can be shared. +The executor's thread pool should be unbounded or set appropriately for the expected use (usually, at least one thread per connection). +If multiple channels are created on each connection, the pool size affects the concurrency, so a variable (or simple cached) thread pool executor would be most suitable. +====== + +It is important to understand that the cache size is (by default) not a limit but is merely the number of channels that can be cached. +With a cache size of, say, 10, any number of channels can actually be in use. +If more than 10 channels are being used and they are all returned to the cache, 10 go in the cache. +The remainder are physically closed. + +Starting with version 1.6, the default channel cache size has been increased from 1 to 25. +In high volume, multi-threaded environments, a small cache means that channels are created and closed at a high rate. +Increasing the default cache size can avoid this overhead. +You should monitor the channels in use through the RabbitMQ Admin UI and consider increasing the cache size further if you +see many channels being created and closed. +The cache grows only on-demand (to suit the concurrency requirements of the application), so this change does not +impact existing low-volume applications. + +Starting with version 1.4.2, the `CachingConnectionFactory` has a property called `channelCheckoutTimeout`. +When this property is greater than zero, the `channelCacheSize` becomes a limit on the number of channels that can be created on a connection. +If the limit is reached, calling threads block until a channel is available or this timeout is reached, in which case a `AmqpTimeoutException` is thrown. + +WARNING: Channels used within the framework (for example, +`RabbitTemplate`) are reliably returned to the cache. +If you create channels outside of the framework, (for example, +by accessing the connections directly and invoking `createChannel()`), you must return them (by closing) reliably, perhaps in a `finally` block, to avoid running out of channels. + +The following example shows how to create a new `connection`: + +[source,java] +---- +CachingConnectionFactory connectionFactory = new CachingConnectionFactory("somehost"); +connectionFactory.setUsername("guest"); +connectionFactory.setPassword("guest"); + +Connection connection = connectionFactory.createConnection(); +---- + +When using XML, the configuration might look like the following example: + +[source,xml] +---- + + + + + +---- + +NOTE: There is also a `SingleConnectionFactory` implementation that is available only in the unit test code of the framework. +It is simpler than `CachingConnectionFactory`, since it does not cache channels, but it is not intended for practical usage outside of simple tests due to its lack of performance and resilience. +If you need to implement your own `ConnectionFactory` for some reason, the `AbstractConnectionFactory` base class may provide a nice starting point. + +A `ConnectionFactory` can be created quickly and conveniently by using the rabbit namespace, as follows: + +[source,xml] +---- + +---- + +In most cases, this approach is preferable, since the framework can choose the best defaults for you. +The created instance is a `CachingConnectionFactory`. +Keep in mind that the default cache size for channels is 25. +If you want more channels to be cached, set a larger value by setting the 'channelCacheSize' property. +In XML it would look like as follows: + +[source,xml] +---- + + + + + + +---- + +Also, with the namespace, you can add the 'channel-cache-size' attribute, as follows: + +[source,xml] +---- + +---- + +The default cache mode is `CHANNEL`, but you can configure it to cache connections instead. +In the following example, we use `connection-cache-size`: + +[source,xml] +---- + +---- + +You can provide host and port attributes by using the namespace, as follows: + +[source,xml] +---- + +---- + +Alternatively, if running in a clustered environment, you can use the addresses attribute, as follows: + +[source,xml] +---- + +---- + +See xref:amqp/connections.adoc#cluster[Connecting to a Cluster] for information about `address-shuffle-mode`. + +The following example with a custom thread factory that prefixes thread names with `rabbitmq-`: + +[source, xml] +---- + + + + + + +---- + +[[addressresolver]] +== AddressResolver + +Starting with version 2.1.15, you can now use an `AddressResolver` to resolve the connection address(es). +This will override any settings of the `addresses` and `host/port` properties. + +[[naming-connections]] +== Naming Connections + +Starting with version 1.7, a `ConnectionNameStrategy` is provided for the injection into the `AbstractionConnectionFactory`. +The generated name is used for the application-specific identification of the target RabbitMQ connection. +The connection name is displayed in the management UI if the RabbitMQ server supports it. +This value does not have to be unique and cannot be used as a connection identifier -- for example, in HTTP API requests. +This value is supposed to be human-readable and is a part of `ClientProperties` under the `connection_name` key. +You can use a simple Lambda, as follows: + +[source, java] +---- +connectionFactory.setConnectionNameStrategy(connectionFactory -> "MY_CONNECTION"); +---- + +The `ConnectionFactory` argument can be used to distinguish target connection names by some logic. +By default, the `beanName` of the `AbstractConnectionFactory`, a hex string representing the object, and an internal counter are used to generate the `connection_name`. +The `` namespace component is also supplied with the `connection-name-strategy` attribute. + +An implementation of `SimplePropertyValueConnectionNameStrategy` sets the connection name to an application property. +You can declare it as a `@Bean` and inject it into the connection factory, as the following example shows: + +[source, java] +---- +@Bean +public SimplePropertyValueConnectionNameStrategy cns() { + return new SimplePropertyValueConnectionNameStrategy("spring.application.name"); +} + +@Bean +public ConnectionFactory rabbitConnectionFactory(ConnectionNameStrategy cns) { + CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); + ... + connectionFactory.setConnectionNameStrategy(cns); + return connectionFactory; +} +---- + +The property must exist in the application context's `Environment`. + +NOTE: When using Spring Boot and its autoconfigured connection factory, you need only declare the `ConnectionNameStrategy` `@Bean`. +Boot auto-detects the bean and wires it into the factory. + +[[blocked-connections-and-resource-constraints]] +== Blocked Connections and Resource Constraints + +The connection might be blocked for interaction from the broker that corresponds to the https://www.rabbitmq.com/memory.html[Memory Alarm]. +Starting with version 2.0, the `org.springframework.amqp.rabbit.connection.Connection` can be supplied with `com.rabbitmq.client.BlockedListener` instances to be notified for connection blocked and unblocked events. +In addition, the `AbstractConnectionFactory` emits a `ConnectionBlockedEvent` and `ConnectionUnblockedEvent`, respectively, through its internal `BlockedListener` implementation. +These let you provide application logic to react appropriately to problems on the broker and (for example) take some corrective actions. + +IMPORTANT: When the application is configured with a single `CachingConnectionFactory`, as it is by default with Spring Boot auto-configuration, the application stops working when the connection is blocked by the Broker. +And when it is blocked by the Broker, any of its clients stop to work. +If we have producers and consumers in the same application, we may end up with a deadlock when producers are blocking the connection (because there are no resources on the Broker any more) and consumers cannot free them (because the connection is blocked). +To mitigate the problem, we suggest having one more separate `CachingConnectionFactory` instance with the same options -- one for producers and one for consumers. +A separate `CachingConnectionFactory` is not possible for transactional producers that execute on a consumer thread, since they should reuse the `Channel` associated with the consumer transactions. + +Starting with version 2.0.2, the `RabbitTemplate` has a configuration option to automatically use a second connection factory, unless transactions are being used. +See xref:amqp/template.adoc#separate-connection[Using a Separate Connection] for more information. +The `ConnectionNameStrategy` for the publisher connection is the same as the primary strategy with `.publisher` appended to the result of calling the method. + +Starting with version 1.7.7, an `AmqpResourceNotAvailableException` is provided, which is thrown when `SimpleConnection.createChannel()` cannot create a `Channel` (for example, because the `channelMax` limit is reached and there are no available channels in the cache). +You can use this exception in the `RetryPolicy` to recover the operation after some back-off. + +[[connection-factory]] +== Configuring the Underlying Client Connection Factory + +The `CachingConnectionFactory` uses an instance of the Rabbit client `ConnectionFactory`. +A number of configuration properties are passed through (`host`, `port`, `userName`, `password`, `requestedHeartBeat`, and `connectionTimeout` for example) when setting the equivalent property on the `CachingConnectionFactory`. +To set other properties (`clientProperties`, for example), you can define an instance of the Rabbit factory and provide a reference to it by using the appropriate constructor of the `CachingConnectionFactory`. +When using the namespace (xref:amqp/connections.adoc[as described earlier]), you need to provide a reference to the configured factory in the `connection-factory` attribute. +For convenience, a factory bean is provided to assist in configuring the connection factory in a Spring application context, as discussed in xref:amqp/connections.adoc#rabbitconnectionfactorybean-configuring-ssl[the next section]. + +[source,xml] +---- + +---- + +NOTE: The 4.0.x client enables automatic recovery by default. +While compatible with this feature, Spring AMQP has its own recovery mechanisms and the client recovery feature generally is not needed. +We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. +You may notice this exception, for example, when a `RetryTemplate` is configured in a `RabbitTemplate`, even when failing over to another broker in a cluster. +Since the auto-recovering connection recovers on a timer, the connection may be recovered more quickly by using Spring AMQP's recovery mechanisms. +Starting with version 1.7.1, Spring AMQP disables `amqp-client` automatic recovery unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. +RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. + +[[rabbitconnectionfactorybean-configuring-ssl]] +== `RabbitConnectionFactoryBean` and Configuring SSL + +Starting with version 1.4, a convenient `RabbitConnectionFactoryBean` is provided to enable convenient configuration of SSL properties on the underlying client connection factory by using dependency injection. +Other setters delegate to the underlying factory. +Previously, you had to configure the SSL options programmatically. +The following example shows how to configure a `RabbitConnectionFactoryBean`: + +[source,java,role=primary] +.Java +---- +@Bean +RabbitConnectionFactoryBean rabbitConnectionFactory() { + RabbitConnectionFactoryBean factoryBean = new RabbitConnectionFactoryBean(); + factoryBean.setUseSSL(true); + factoryBean.setSslPropertiesLocation(new ClassPathResource("secrets/rabbitSSL.properties")); + return factoryBean; +} + +@Bean +CachingConnectionFactory connectionFactory(ConnectionFactory rabbitConnectionFactory) { + CachingConnectionFactory ccf = new CachingConnectionFactory(rabbitConnectionFactory); + ccf.setHost("..."); + // ... + return ccf; +} +---- +[source,properties,role=secondary] +.Boot application.properties +---- +spring.rabbitmq.ssl.enabled:true +spring.rabbitmq.ssl.keyStore=... +spring.rabbitmq.ssl.keyStoreType=jks +spring.rabbitmq.ssl.keyStorePassword=... +spring.rabbitmq.ssl.trustStore=... +spring.rabbitmq.ssl.trustStoreType=jks +spring.rabbitmq.ssl.trustStorePassword=... +spring.rabbitmq.host=... +... +---- +[source,xml,role=secondary] +.XML +---- + + + + + + +---- + +See the https://www.rabbitmq.com/ssl.html[RabbitMQ Documentation] for information about configuring SSL. +Omit the `keyStore` and `trustStore` configuration to connect over SSL without certificate validation. +The next example shows how you can provide key and trust store configuration. + +The `sslPropertiesLocation` property is a Spring `Resource` pointing to a properties file containing the following keys: + +[source] +---- +keyStore=file:/secret/keycert.p12 +trustStore=file:/secret/trustStore +keyStore.passPhrase=secret +trustStore.passPhrase=secret +---- + +The `keyStore` and `truststore` are Spring `Resources` pointing to the stores. +Typically this properties file is secured by the operating system with the application having read access. + +Starting with Spring AMQP version 1.5,you can set these properties directly on the factory bean. +If both discrete properties and `sslPropertiesLocation` is provided, properties in the latter override the +discrete values. + +IMPORTANT: Starting with version 2.0, the server certificate is validated by default because it is more secure. +If you wish to skip this validation for some reason, set the factory bean's `skipServerCertificateValidation` property to `true`. +Starting with version 2.1, the `RabbitConnectionFactoryBean` now calls `enableHostnameVerification()` by default. +To revert to the previous behavior, set the `enableHostnameVerification` property to `false`. + +IMPORTANT: Starting with version 2.2.5, the factory bean will always use TLS v1.2 by default; previously, it used v1.1 in some cases and v1.2 in others (depending on other properties). +If you need to use v1.1 for some reason, set the `sslAlgorithm` property: `setSslAlgorithm("TLSv1.1")`. + +[[cluster]] +== Connecting to a Cluster + +To connect to a cluster, configure the `addresses` property on the `CachingConnectionFactory`: + +[source, java] +---- +@Bean +public CachingConnectionFactory ccf() { + CachingConnectionFactory ccf = new CachingConnectionFactory(); + ccf.setAddresses("host1:5672,host2:5672,host3:5672"); + return ccf; +} +---- + +Starting with version 3.0, the underlying connection factory will attempt to connect to a host, by choosing a random address, whenever a new connection is established. +To revert to the previous behavior of attempting to connect from first to last, set the `addressShuffleMode` property to `AddressShuffleMode.NONE`. + +Starting with version 2.3, the `INORDER` shuffle mode was added, which means the first address is moved to the end after a connection is created. +You may wish to use this mode with the {rabbitmq-server-github}/rabbitmq_sharding[RabbitMQ Sharding Plugin] with `CacheMode.CONNECTION` and suitable concurrency if you wish to consume from all shards on all nodes. + +[source, java] +---- +@Bean +public CachingConnectionFactory ccf() { + CachingConnectionFactory ccf = new CachingConnectionFactory(); + ccf.setAddresses("host1:5672,host2:5672,host3:5672"); + ccf.setAddressShuffleMode(AddressShuffleMode.INORDER); + return ccf; +} +---- + +[[routing-connection-factory]] +== Routing Connection Factory + +Starting with version 1.3, the `AbstractRoutingConnectionFactory` has been introduced. +This factory provides a mechanism to configure mappings for several `ConnectionFactories` and determine a target `ConnectionFactory` by some `lookupKey` at runtime. +Typically, the implementation checks a thread-bound context. +For convenience, Spring AMQP provides the `SimpleRoutingConnectionFactory`, which gets the current thread-bound `lookupKey` from the `SimpleResourceHolder`. +The following examples shows how to configure a `SimpleRoutingConnectionFactory` in both XML and Java: + +[source,xml] +---- + + + + + + + + + + +---- + +[source,java] +---- +public class MyService { + + @Autowired + private RabbitTemplate rabbitTemplate; + + public void service(String vHost, String payload) { + SimpleResourceHolder.bind(rabbitTemplate.getConnectionFactory(), vHost); + rabbitTemplate.convertAndSend(payload); + SimpleResourceHolder.unbind(rabbitTemplate.getConnectionFactory()); + } + +} +---- + +It is important to unbind the resource after use. +For more information, see the javadoc:org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory[JavaDoc] for `AbstractRoutingConnectionFactory`. + +Starting with version 1.4, `RabbitTemplate` supports the SpEL `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` properties, which are evaluated on each AMQP protocol interaction operation (`send`, `sendAndReceive`, `receive`, or `receiveAndReply`), resolving to a `lookupKey` value for the provided `AbstractRoutingConnectionFactory`. +You can use bean references, such as `@vHostResolver.getVHost(#root)` in the expression. +For `send` operations, the message to be sent is the root evaluation object. +For `receive` operations, the `queueName` is the root evaluation object. + +The routing algorithm is as follows: If the selector expression is `null` or is evaluated to `null` or the provided `ConnectionFactory` is not an instance of `AbstractRoutingConnectionFactory`, everything works as before, relying on the provided `ConnectionFactory` implementation. +The same occurs if the evaluation result is not `null`, but there is no target `ConnectionFactory` for that `lookupKey` and the `AbstractRoutingConnectionFactory` is configured with `lenientFallback = true`. +In the case of an `AbstractRoutingConnectionFactory`, it does fallback to its `routing` implementation based on `determineCurrentLookupKey()`. +However, if `lenientFallback = false`, an `IllegalStateException` is thrown. + +The namespace support also provides the `send-connection-factory-selector-expression` and `receive-connection-factory-selector-expression` attributes on the `` component. + +Also, starting with version 1.4, you can configure a routing connection factory in a listener container. +In that case, the list of queue names is used as the lookup key. +For example, if you configure the container with `setQueueNames("thing1", "thing2")`, the lookup key is `[thing1,thing]"` (note that there is no space in the key). + +Starting with version 1.6.9, you can add a qualifier to the lookup key by using `setLookupKeyQualifier` on the listener container. +Doing so enables, for example, listening to queues with the same name but in a different virtual host (where you would have a connection factory for each). + +For example, with lookup key qualifier `thing1` and a container listening to queue `thing2`, the lookup key you could register the target connection factory with could be `thing1[thing2]`. + +IMPORTANT: The target (and default, if provided) connection factories must have the same settings for publisher confirms and returns. +See xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns]. + +Starting with version 2.4.4, this validation can be disabled. +If you have a case that the values between confirms and returns need to be unequal, you can use `AbstractRoutingConnectionFactory#setConsistentConfirmsReturns` to turn of the validation. +Note that the first connection factory added to `AbstractRoutingConnectionFactory` will determine the general values of `confirms` and `returns`. + +It may be useful if you have a case that certain messages you would to check confirms/returns and others you don't. +For example: + +[source, java] +---- +@Bean +public RabbitTemplate rabbitTemplate() { + final com.rabbitmq.client.ConnectionFactory cf = new com.rabbitmq.client.ConnectionFactory(); + cf.setHost("localhost"); + cf.setPort(5672); + + CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(cf); + cachingConnectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED); + + PooledChannelConnectionFactory pooledChannelConnectionFactory = new PooledChannelConnectionFactory(cf); + + final Map connectionFactoryMap = new HashMap<>(2); + connectionFactoryMap.put("true", cachingConnectionFactory); + connectionFactoryMap.put("false", pooledChannelConnectionFactory); + + final AbstractRoutingConnectionFactory routingConnectionFactory = new SimpleRoutingConnectionFactory(); + routingConnectionFactory.setConsistentConfirmsReturns(false); + routingConnectionFactory.setDefaultTargetConnectionFactory(pooledChannelConnectionFactory); + routingConnectionFactory.setTargetConnectionFactories(connectionFactoryMap); + + final RabbitTemplate rabbitTemplate = new RabbitTemplate(routingConnectionFactory); + + final Expression sendExpression = new SpelExpressionParser().parseExpression( + "messageProperties.headers['x-use-publisher-confirms'] ?: false"); + rabbitTemplate.setSendConnectionFactorySelectorExpression(sendExpression); +} +---- + +This way messages with the header `x-use-publisher-confirms: true` will be sent through the caching connection, and you can ensure the message delivery. +See xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns] for more information about ensuring message delivery. + +[[queue-affinity]] +== Queue Affinity and the `LocalizedQueueConnectionFactory` + +When using HA queues in a cluster, for the best performance, you may want to connect to the physical broker +where the lead queue resides. +The `CachingConnectionFactory` can be configured with multiple broker addresses. +This is to fail over and the client attempts to connect in accordance with the configured `AddressShuffleMode` order. +The `LocalizedQueueConnectionFactory` uses the REST API provided by the management plugin to determine which node is the lead for the queue. +It then creates (or retrieves from a cache) a `CachingConnectionFactory` that connects to just that node. +If the connection fails, the new lead node is determined and the consumer connects to it. +The `LocalizedQueueConnectionFactory` is configured with a default connection factory, in case the physical location of the queue cannot be determined, in which case it connects as normal to the cluster. + +The `LocalizedQueueConnectionFactory` is a `RoutingConnectionFactory` and the `SimpleMessageListenerContainer` uses the queue names as the lookup key as discussed in <> above. + +NOTE: For this reason (the use of the queue name for the lookup), the `LocalizedQueueConnectionFactory` can only be used if the container is configured to listen to a single queue. + +NOTE: The RabbitMQ management plugin must be enabled on each node. + +CAUTION: This connection factory is intended for long-lived connections, such as those used by the `SimpleMessageListenerContainer`. +It is not intended for short connection use, such as with a `RabbitTemplate` because of the overhead of invoking the REST API before making the connection. +Also, for publish operations, the queue is unknown, and the message is published to all cluster members anyway, so the logic of looking up the node has little value. + +The following example configuration shows how to configure the factories: + +[source, java] +---- +@Autowired +private ConfigurationProperties props; + +@Bean +public CachingConnectionFactory defaultConnectionFactory() { + CachingConnectionFactory cf = new CachingConnectionFactory(); + cf.setAddresses(this.props.getAddresses()); + cf.setUsername(this.props.getUsername()); + cf.setPassword(this.props.getPassword()); + cf.setVirtualHost(this.props.getVirtualHost()); + return cf; +} + +@Bean +public LocalizedQueueConnectionFactory queueAffinityCF( + @Qualifier("defaultConnectionFactory") ConnectionFactory defaultCF) { + return new LocalizedQueueConnectionFactory(defaultCF, + StringUtils.commaDelimitedListToStringArray(this.props.getAddresses()), + StringUtils.commaDelimitedListToStringArray(this.props.getAdminUris()), + StringUtils.commaDelimitedListToStringArray(this.props.getNodes()), + this.props.getVirtualHost(), this.props.getUsername(), this.props.getPassword(), + false, null); +} +---- + +Notice that the first three parameters are arrays of `addresses`, `adminUris`, and `nodes`. +These are positional in that, when a container attempts to connect to a queue, it uses the admin API to determine which node is the lead for the queue and connects to the address in the same array position as that node. + +IMPORTANT: Starting with version 3.0, the RabbitMQ `http-client` is no longer used to access the Rest API. +Instead, by default, the `WebClient` from Spring Webflux is used if `spring-webflux` is on the class path; otherwise a `RestTemplate` is used. + +To add `WebFlux` to the class path: + +.Maven +[source,xml,subs="+attributes"] +---- + + org.springframework.amqp + spring-rabbit + +---- +.Gradle +[source,groovy,subs="+attributes"] +---- +compile 'org.springframework.amqp:spring-rabbit' +---- + +You can also use other REST technology by implementing `LocalizedQueueConnectionFactory.NodeLocator` and overriding its `createClient, ``restCall`, and optionally, `close` methods. + +[source, java] +---- +lqcf.setNodeLocator(new NodeLocator() { + + @Override + public MyClient createClient(String userName, String password) { + ... + } + + @Override + public HashMap restCall(MyClient client, URI uri) { + ... + }); + +}); +---- + +The framework provides the `WebFluxNodeLocator` and `RestTemplateNodeLocator`, with the default as discussed above. + +[[cf-pub-conf-ret]] +== Publisher Confirms and Returns + +Confirmed (with correlation) and returned messages are supported by setting the `CachingConnectionFactory` property `publisherConfirmType` to `ConfirmType.CORRELATED` and the `publisherReturns` property to 'true'. + +When these options are set, `Channel` instances created by the factory are wrapped in an `PublisherCallbackChannel`, which is used to facilitate the callbacks. +When such a channel is obtained, the client can register a `PublisherCallbackChannel.Listener` with the `Channel`. +The `PublisherCallbackChannel` implementation contains logic to route a confirm or return to the appropriate listener. +These features are explained further in the following sections. + +See also xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and `simplePublisherConfirms` in xref:amqp/template.adoc#scoped-operations[Scoped Operations]. + +TIP: For some more background information, see the blog post by the RabbitMQ team titled https://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms/[Introducing Publisher Confirms]. + +[[connection-channel-listeners]] +== Connection and Channel Listeners + +The connection factory supports registering `ConnectionListener` and `ChannelListener` implementations. +This allows you to receive notifications for connection and channel related events. +(A `ConnectionListener` is used by the `RabbitAdmin` to perform declarations when the connection is established - see xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#automatic-declaration[Automatic Declaration of Exchanges, Queues, and Bindings] for more information). +The following listing shows the `ConnectionListener` interface definition: + +[source, java] +---- +@FunctionalInterface +public interface ConnectionListener { + + void onCreate(Connection connection); + + default void onClose(Connection connection) { + } + + default void onShutDown(ShutdownSignalException signal) { + } + +} +---- + +Starting with version 2.0, the `org.springframework.amqp.rabbit.connection.Connection` object can be supplied with `com.rabbitmq.client.BlockedListener` instances to be notified for connection blocked and unblocked events. +The following example shows the ChannelListener interface definition: + +[source, java] +---- +@FunctionalInterface +public interface ChannelListener { + + void onCreate(Channel channel, boolean transactional); + + default void onShutDown(ShutdownSignalException signal) { + } + +} +---- + +See xref:amqp/template.adoc#publishing-is-async[Publishing is Asynchronous -- How to Detect Successes and Failures] for one scenario where you might want to register a `ChannelListener`. + +[[channel-close-logging]] +== Logging Channel Close Events + +Version 1.5 introduced a mechanism to enable users to control logging levels. + +The `AbstractConnectionFactory` uses a default strategy to log channel closures as follows: + +* Normal channel closes (200 OK) are not logged. +* If a channel is closed due to a failed passive queue declaration, it is logged at DEBUG level. +* If a channel is closed because the `basic.consume` is refused due to an exclusive consumer condition, it is logged at +DEBUG level (since 3.1, previously INFO). +* All others are logged at ERROR level. + +To modify this behavior, you can inject a custom `ConditionalExceptionLogger` into the +`CachingConnectionFactory` in its `closeExceptionLogger` property. + +Also, the `AbstractConnectionFactory.DefaultChannelCloseLogger` is now public, allowing it to be sub classed. + +See also xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events]. + +[[runtime-cache-properties]] +== Runtime Cache Properties + +Staring with version 1.6, the `CachingConnectionFactory` now provides cache statistics through the `getCacheProperties()` +method. +These statistics can be used to tune the cache to optimize it in production. +For example, the high water marks can be used to determine whether the cache size should be increased. +If it equals the cache size, you might want to consider increasing further. +The following table describes the `CacheMode.CHANNEL` properties: + +.Cache properties for CacheMode.CHANNEL +[cols="2l,4", options="header"] +|=== +|Property + +|Meaning + +|connectionName + +|The name of the connection generated by the `ConnectionNameStrategy`. + +|channelCacheSize + +|The currently configured maximum channels that are allowed to be idle. + +|localPort + +|The local port for the connection (if available). +This can be used to correlate with connections and channels on the RabbitMQ Admin UI. + +|idleChannelsTx + +|The number of transactional channels that are currently idle (cached). + +|idleChannelsNotTx + +|The number of non-transactional channels that are currently idle (cached). + +|idleChannelsTxHighWater + +|The maximum number of transactional channels that have been concurrently idle (cached). + +|idleChannelsNotTxHighWater + +|The maximum number of non-transactional channels have been concurrently idle (cached). + +|=== + +The following table describes the `CacheMode.CONNECTION` properties: + +.Cache properties for CacheMode.CONNECTION +[cols="2l,4", options="header"] +|=== +|Property + +|Meaning + +|connectionName: + +|The name of the connection generated by the `ConnectionNameStrategy`. + +|openConnections + +|The number of connection objects representing connections to brokers. + +|channelCacheSize + +|The currently configured maximum channels that are allowed to be idle. + +|connectionCacheSize + +|The currently configured maximum connections that are allowed to be idle. + +|idleConnections + +|The number of connections that are currently idle. + +|idleConnectionsHighWater + +|The maximum number of connections that have been concurrently idle. + +|idleChannelsTx: + +|The number of transactional channels that are currently idle (cached) for this connection. +You can use the `localPort` part of the property name to correlate with connections and channels on the RabbitMQ Admin UI. + +|idleChannelsNotTx: + +|The number of non-transactional channels that are currently idle (cached) for this connection. +The `localPort` part of the property name can be used to correlate with connections and channels on the RabbitMQ Admin UI. + +|idleChannelsTxHighWater: + +|The maximum number of transactional channels that have been concurrently idle (cached). +The localPort part of the property name can be used to correlate with connections and channels on the RabbitMQ Admin UI. + +|idleChannelsNotTxHighWater: + +|The maximum number of non-transactional channels have been concurrently idle (cached). +You can use the `localPort` part of the property name to correlate with connections and channels on the RabbitMQ Admin UI. + +|=== + +The `cacheMode` property (`CHANNEL` or `CONNECTION`) is also included. + +.JVisualVM Example +image::cacheStats.png[align="center"] + +[[auto-recovery]] +== RabbitMQ Automatic Connection/Topology recovery + +Since the first version of Spring AMQP, the framework has provided its own connection and channel recovery in the event of a broker failure. +Also, as discussed in xref:amqp/broker-configuration.adoc[Configuring the Broker], the `RabbitAdmin` re-declares any infrastructure beans (queues and others) when the connection is re-established. +It therefore does not rely on the https://www.rabbitmq.com/api-guide.html#recovery[auto-recovery] that is now provided by the `amqp-client` library. +The `amqp-client`, has auto recovery enabled by default. +There are some incompatibilities between the two recovery mechanisms so, by default, Spring sets the `automaticRecoveryEnabled` property on the underlying `RabbitMQ connectionFactory` to `false`. +Even if the property is `true`, Spring effectively disables it, by immediately closing any recovered connections. + +IMPORTANT: By default, only elements (queues, exchanges, bindings) that are defined as beans will be re-declared after a connection failure. +See xref:amqp/broker-configuration.adoc#declarable-recovery[Recovering Auto-Delete Declarations] for how to change that behavior. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc new file mode 100644 index 0000000000..80440df9c7 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc @@ -0,0 +1,746 @@ +[[containerAttributes]] += Message Listener Container Configuration + +There are quite a few options for configuring a `SimpleMessageListenerContainer` (SMLC) and a `DirectMessageListenerContainer` (DMLC) related to transactions and quality of service, and some of them interact with each other. +Properties that apply to the SMLC, DMLC, or `StreamListenerContainer` (StLC) (see xref:stream.adoc[Using the RabbitMQ Stream Plugin]) are indicated by the check mark in the appropriate column. +See xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container] for information to help you decide which container is appropriate for your application. + +The following table shows the container property names and their equivalent attribute names (in parentheses) when using the namespace to configure a ``. +The `type` attribute on that element can be `simple` (default) or `direct` to specify an `SMLC` or `DMLC` respectively. +Some properties are not exposed by the namespace. +These are indicated by `N/A` for the attribute. + +.Configuration options for a message listener container +[cols="8,16,1,1,1", options="header"] +|=== +|Property +(Attribute) +|Description +|SMLC +|DMLC +|StLC + +|[[ackTimeout]]<> + +(N/A) + +|When `messagesPerAck` is set, this timeout is used as an alternative to send an ack. +When a new message arrives, the count of unacked messages is compared to `messagesPerAck`, and the time since the last ack is compared to this value. +If either condition is `true`, the message is acknowledged. +When no new messages arrive and there are unacked messages, this timeout is approximate since the condition is only checked each `monitorInterval`. +See also `messagesPerAck` and `monitorInterval` in this table. + +a| +a|image::tickmark.png[] +a| + +|[[acknowledgeMode]]<> + +(acknowledge) + +a| +* `NONE`: No acks are sent (incompatible with `channelTransacted=true`). +RabbitMQ calls this "`autoack`", because the broker assumes all messages are acked without any action from the consumer. +* `MANUAL`: The listener must acknowledge all messages by calling `Channel.basicAck()`. +* `AUTO`: The container acknowledges the message automatically, unless the `MessageListener` throws an exception. +Note that `acknowledgeMode` is complementary to `channelTransacted` -- if the channel is transacted, the broker requires a commit notification in addition to the ack. +This is the default mode. +See also `batchSize`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[adviceChain]]<> + +(advice-chain) + +|An array of AOP Advice to apply to the listener execution. +This can be used to apply additional cross-cutting concerns, such as automatic retry in the event of broker death. +Note that simple re-connection after an AMQP error is handled by the `CachingConnectionFactory`, as long as the broker is still alive. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[afterReceivePostProcessors]]<> + +(N/A) + +|An array of `MessagePostProcessor` instances that are invoked before invoking the listener. +Post processors can implement `PriorityOrdered` or `Ordered`. +The array is sorted with un-ordered members invoked last. +If a post processor returns `null`, the message is discarded (and acknowledged, if appropriate). + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[alwaysRequeueWithTxManagerRollback]]<> + +(N/A) + +|Set to `true` to always requeue messages on rollback when a transaction manager is configured. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[autoDeclare]]<> + +(auto-declare) + +a|When set to `true` (default), the container uses a `RabbitAdmin` to redeclare all AMQP objects (queues, exchanges, bindings), if it detects that at least one of its queues is missing during startup, perhaps because it is an `auto-delete` or an expired queue, but the redeclaration proceeds if the queue is missing for any reason. +To disable this behavior, set this property to `false`. +Note that the container fails to start if all of its queues are missing. + +NOTE: Prior to version 1.6, if there was more than one admin in the context, the container would randomly select one. +If there were no admins, it would create one internally. +In either case, this could cause unexpected results. +Starting with version 1.6, for `autoDeclare` to work, there must be exactly one `RabbitAdmin` in the context, or a reference to a specific instance must be configured on the container using the `rabbitAdmin` property. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[autoStartup]]<> + +(auto-startup) + +|Flag to indicate that the container should start when the `ApplicationContext` does (as part of the `SmartLifecycle` callbacks, which happen after all beans are initialized). +Defaults to `true`, but you can set it to `false` if your broker might not be available on startup and call `start()` later manually when you know the broker is ready. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a|image::tickmark.png[] + +|[[batchSize]]<> + +(transaction-size) +(batch-size) + +|When used with `acknowledgeMode` set to `AUTO`, the container tries to process up to this number of messages before sending an ack (waiting for each one up to the receive timeout setting). +This is also when a transactional channel is committed. +If the `prefetchCount` is less than the `batchSize`, it is increased to match the `batchSize`. + +a|image::tickmark.png[] +a| +a| + +|[[batchingStrategy]]<> + +(N/A) + +|The strategy used when debatchng messages. +Default `SimpleDebatchingStrategy`. +See xref:amqp/sending-messages.adoc#template-batching[Batching] and xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching]. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[channelTransacted]]<> + +(channel-transacted) + +|Boolean flag to signal that all messages should be acknowledged in a transaction (either manually or automatically). + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[concurrency]]<> + +(N/A) + +|`m-n` The range of concurrent consumers for each listener (min, max). +If only `n` is provided, `n` is a fixed number of consumers. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. + +a|image::tickmark.png[] +a| +a| + +|[[concurrentConsumers]]<> + +(concurrency) + +|The number of concurrent consumers to initially start for each listener. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. +For the `StLC`, concurrency is controlled via an overloaded `superStream` method; see xref:stream.adoc#super-stream-consumer[Consuming Super Streams with Single Active Consumers]. + +a|image::tickmark.png[] +a| +a|image::tickmark.png[] + +|[[connectionFactory]]<> + +(connection-factory) + +|A reference to the `ConnectionFactory`. +When configuring by using the XML namespace, the default referenced bean name is `rabbitConnectionFactory`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[consecutiveActiveTrigger]]<> + +(min-consecutive-active) + +|The minimum number of consecutive messages received by a consumer, without a receive timeout occurring, when considering starting a new consumer. +Also impacted by 'batchSize'. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. +Default: 10. + +a|image::tickmark.png[] +a| +a| + +|[[consecutiveIdleTrigger]]<> + +(min-consecutive-idle) + +|The minimum number of receive timeouts a consumer must experience before considering stopping a consumer. +Also impacted by 'batchSize'. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. +Default: 10. + +a|image::tickmark.png[] +a| +a| + +|[[consumerBatchEnabled]]<> + +(batch-enabled) + +|If the `MessageListener` supports it, setting this to true enables batching of discrete messages, up to `batchSize`; a partial batch will be delivered if no new messages arrive in `receiveTimeout` or gathering batch messages time exceeded `batchReceiveTimeout`. +When this is false, batching is only supported for batches created by a producer; see xref:amqp/sending-messages.adoc#template-batching[Batching]. + +a|image::tickmark.png[] +a| +a| + +|[[consumerCustomizer]]<> + +(N/A) + +|A `ConsumerCustomizer` bean used to modify stream consumers created by the container. + +a| +a| +a|image::tickmark.png[] + +|[[consumerStartTimeout]]<> + +(N/A) + +|The time in milliseconds to wait for a consumer thread to start. +If this time elapses, an error log is written. +An example of when this might happen is if a configured `taskExecutor` has insufficient threads to support the container `concurrentConsumers`. + +See xref:amqp/receiving-messages/threading.adoc[Threading and Asynchronous Consumers]. +Default: 60000 (one minute). + +a|image::tickmark.png[] +a| +a| + +|[[consumerTagStrategy]]<> + +(consumer-tag-strategy) + +|Set an implementation of xref:amqp/receiving-messages/consumerTags.adoc[ConsumerTagStrategy], enabling the creation of a (unique) tag for each consumer. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[consumersPerQueue]]<> + +(consumers-per-queue) + +|The number of consumers to create for each configured queue. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. + +a| +a|image::tickmark.png[] +a| + +|[[consumeDelay]]<> + +(N/A) + +|When using the {rabbitmq-server-github}/rabbitmq_sharding[RabbitMQ Sharding Plugin] with `concurrentConsumers > 1`, there is a race condition that can prevent even distribution of the consumers across the shards. +Use this property to add a small delay between consumer starts to avoid this race condition. +You should experiment with values to determine the suitable delay for your environment. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[debatchingEnabled]]<> + +(N/A) + +|When true, the listener container will debatch batched messages and invoke the listener with each message from the batch. +Starting with version 2.2.7, xref:amqp/sending-messages.adoc#template-batching[producer created batches] will be debatched as a `List` if the listener is a `BatchMessageListener` or `ChannelAwareBatchMessageListener`. +Otherwise messages from the batch are presented one-at-a-time. +Default true. +See xref:amqp/sending-messages.adoc#template-batching[Batching] and xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching]. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[declarationRetries]]<> + +(declaration-retries) + +|The number of retry attempts when passive queue declaration fails. +Passive queue declaration occurs when the consumer starts or, when consuming from multiple queues, when not all queues were available during initialization. +When none of the configured queues can be passively declared (for any reason) after the retries are exhausted, the container behavior is controlled by the `missingQueuesFatal` property, described earlier. +Default: Three retries (for a total of four attempts). + +a|image::tickmark.png[] +a| +a| + +|[[defaultRequeueRejected]]<> + +(requeue-rejected) + +|Determines whether messages that are rejected because the listener threw an exception should be requeued or not. +Default: `true`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[errorHandler]]<> + +(error-handler) + +|A reference to an `ErrorHandler` strategy for handling any uncaught exceptions that may occur during the execution of the MessageListener. +Default: `ConditionalRejectingErrorHandler` + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[exclusive]]<> + +(exclusive) + +|Determines whether the single consumer in this container has exclusive access to the queues. +The concurrency of the container must be 1 when this is `true`. +If another consumer has exclusive access, the container tries to recover the consumer, according to the +`recovery-interval` or `recovery-back-off`. +When using the namespace, this attribute appears on the `` element along with the queue names. +Default: `false`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[exclusiveConsumerExceptionLogger]]<> + +(N/A) + +|An exception logger used when an exclusive consumer cannot gain access to a queue. +By default, this is logged at the `WARN` level. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[failedDeclarationRetryInterval]]<> + +(failed-declaration +-retry-interval) + +|The interval between passive queue declaration retry attempts. +Passive queue declaration occurs when the consumer starts or, when consuming from multiple queues, when not all queues were available during initialization. +Default: 5000 (five seconds). + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[forceCloseChannel]]<> + +(N/A) + +|If the consumers do not respond to a shutdown within `shutdownTimeout`, if this is `true`, the channel will be closed, causing any unacked messages to be requeued. +Defaults to `true` since 2.0. +You can set it to `false` to revert to the previous behavior. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[forceStop]]<> + +(N/A) + +|Set to true to stop (when the container is stopped) after the current record is processed; causing all prefetched messages to be requeued. +By default, the container will cancel the consumer and process all prefetched messages before stopping. +Since versions 2.4.14, 3.0.6 +Defaults to `false`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[globalQos]]<> + +(global-qos) + +|When true, the `prefetchCount` is applied globally to the channel rather than to each consumer on the channel. +See https://www.rabbitmq.com/amqp-0-9-1-reference.html#basic.qos.global[`basicQos.global`] for more information. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|(group) + +|This is available only when using the namespace. +When specified, a bean of type `Collection` is registered with this name, and the +container for each `` element is added to the collection. +This allows, for example, starting and stopping the group of containers by iterating over the collection. +If multiple `` elements have the same group value, the containers in the collection form +an aggregate of all containers so designated. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[idleEventInterval]]<> + +(idle-event-interval) + +|See xref:amqp/receiving-messages/idle-containers.adoc[Detecting Idle Asynchronous Consumers]. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[javaLangErrorHandler]]<> + +(N/A) + +|An `AbstractMessageListenerContainer.JavaLangErrorHandler` implementation that is called when a container thread catches an `Error`. +The default implementation calls `System.exit(99)`; to revert to the previous behavior (do nothing), add a no-op handler. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[maxConcurrentConsumers]]<> + +(max-concurrency) + +|The maximum number of concurrent consumers to start, if needed, on demand. +Must be greater than or equal to 'concurrentConsumers'. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. + +a|image::tickmark.png[] +a| +a| + +|[[messagesPerAck]]<> + +(N/A) + +|The number of messages to receive between acks. +Use this to reduce the number of acks sent to the broker (at the cost of increasing the possibility of redelivered messages). +Generally, you should set this property only on high-volume listener containers. +If this is set and a message is rejected (exception thrown), pending acks are acknowledged and the failed message is rejected. +Not allowed with transacted channels. +If the `prefetchCount` is less than the `messagesPerAck`, it is increased to match the `messagesPerAck`. +Default: ack every message. +See also `ackTimeout` in this table. + +a| +a|image::tickmark.png[] +a| + +|[[mismatchedQueuesFatal]]<> + +(mismatched-queues-fatal) + +a|When the container starts, if this property is `true` (default: `false`), the container checks that all queues declared in the context are compatible with queues already on the broker. +If mismatched properties (such as `auto-delete`) or arguments (such as `x-message-ttl`) exist, the container (and application context) fails to start with a fatal exception. + +If the problem is detected during recovery (for example, after a lost connection), the container is stopped. + +There must be a single `RabbitAdmin` in the application context (or one specifically configured on the container by using the `rabbitAdmin` property). +Otherwise, this property must be `false`. + +NOTE: If the broker is not available during initial startup, the container starts and the conditions are checked when the connection is established. + +IMPORTANT: The check is done against all queues in the context, not just the queues that a particular listener is configured to use. +If you wish to limit the checks to just those queues used by a container, you should configure a separate `RabbitAdmin` for the container, and provide a reference to it using the `rabbitAdmin` property. +See xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration] for more information. + +IMPORTANT: Mismatched queue argument detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`. +This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds. +Applications using lazy listener beans should check the queue arguments before getting a reference to the lazy bean. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[missingQueuesFatal]]<> + +(missing-queues-fatal) + +a|When set to `true` (default), if none of the configured queues are available on the broker, it is considered fatal. +This causes the application context to fail to initialize during startup. +Also, when the queues are deleted while the container is running, by default, the consumers make three retries to connect to the queues (at five second intervals) and stop the container if these attempts fail. + +This was not configurable in previous versions. + +When set to `false`, after making the three retries, the container goes into recovery mode, as with other problems, such as the broker being down. +The container tries to recover according to the `recoveryInterval` property. +During each recovery attempt, each consumer again tries four times to passively declare the queues at five second intervals. +This process continues indefinitely. + +You can also use a properties bean to set the property globally for all containers, as follows: + +[source,xml] +---- + + + false + + +---- + +This global property is not applied to any containers that have an explicit `missingQueuesFatal` property set. + +The default retry properties (three retries at five-second intervals) can be overridden by setting the properties below. + +IMPORTANT: Missing queue detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`. +This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds. +Applications using lazy listener beans should check the queue(s) before getting a reference to the lazy bean. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[monitorInterval]]<> + +(monitor-interval) + +|With the DMLC, a task is scheduled to run at this interval to monitor the state of the consumers and recover any that have failed. + +a| +a|image::tickmark.png[] +a| + +|[[noLocal]]<> + +(N/A) + +|Set to `true` to disable delivery from the server to consumers messages published on the same channel's connection. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[phase]]<> + +(phase) + +|When `autoStartup` is `true`, the lifecycle phase within which this container should start and stop. +The lower the value, the earlier this container starts and the later it stops. +The default is `Integer.MAX_VALUE`, meaning the container starts as late as possible and stops as soon as possible. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[possibleAuthenticationFailureFatal]]<> + +(possible-authentication-failure-fatal) + +a|When set to `true` (default for SMLC), if a `PossibleAuthenticationFailureException` is thrown during connection, it is considered fatal. +This causes the application context to fail to initialize during startup (if the container is configured with auto startup). + +Since _version 2.0_. + +**DirectMessageListenerContainer** + +When set to `false` (default), each consumer will attempt to reconnect according to the `monitorInterval`. + +**SimpleMessageListenerContainer** + +When set to `false`, after making the 3 retries, the container will go into recovery mode, as with other problems, such as the broker being down. +The container will attempt to recover according to the `recoveryInterval` property. +During each recovery attempt, each consumer will again try 4 times to start. +This process will continue indefinitely. + +You can also use a properties bean to set the property globally for all containers, as follows: + +[source,xml] +---- + + + false + + +---- + +This global property will not be applied to any containers that have an explicit `missingQueuesFatal` property set. + +The default retry properties (3 retries at 5 second intervals) can be overridden using the properties after this one. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[prefetchCount]]<> + +(prefetch) + +a|The number of unacknowledged messages that can be outstanding at each consumer. +The higher this value is, the faster the messages can be delivered, but the higher the risk of non-sequential processing. +Ignored if the `acknowledgeMode` is `NONE`. +This is increased, if necessary, to match the `batchSize` or `messagePerAck`. +Defaults to 250 since 2.0. +You can set it to 1 to revert to the previous behavior. + +IMPORTANT: There are scenarios where the prefetch value should +be low -- for example, with large messages, especially if the processing is slow (messages could add up +to a large amount of memory in the client process), and if strict message ordering is necessary +(the prefetch value should be set back to 1 in this case). +Also, with low-volume messaging and multiple consumers (including concurrency within a single listener container instance), you may wish to reduce the prefetch to get a more even distribution of messages across consumers. + +Also see `globalQos`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[rabbitAdmin]]<> + +(admin) + +|When a listener container listens to at least one auto-delete queue and it is found to be missing during startup, the container uses a `RabbitAdmin` to declare the queue and any related bindings and exchanges. +If such elements are configured to use conditional declaration (see xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration]), the container must use the admin that was configured to declare those elements. +Specify that admin here. +It is required only when using auto-delete queues with conditional declaration. +If you do not wish the auto-delete queues to be declared until the container is started, set `auto-startup` to `false` on the admin. +Defaults to a `RabbitAdmin` that declares all non-conditional elements. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[receiveTimeout]]<> + +(receive-timeout) + +|The maximum time to wait for each message. +If `acknowledgeMode=NONE`, this has very little effect -- the container spins round and asks for another message. +It has the biggest effect for a transactional `Channel` with `batchSize > 1`, since it can cause messages already consumed not to be acknowledged until the timeout expires. +When `consumerBatchEnabled` is true, a partial batch will be delivered if this timeout occurs before a batch is complete. + +a|image::tickmark.png[] +a| +a| + +|[[batchReceiveTimeout]]<> + +(batch-receive-timeout) + +|The number of milliseconds of timeout for gathering batch messages. +It limits the time to wait to fill batchSize. +When `batchSize > 1` and the time to gathering batch messages is greater than `batchReceiveTime`, batch will be delivered. +Default is 0 (no timeout). + +a|image::tickmark.png[] +a| +a| + +|[[recoveryBackOff]]<> + +(recovery-back-off) + +|Specifies the `BackOff` for intervals between attempts to start a consumer if it fails to start for non-fatal reasons. +Default is `FixedBackOff` with unlimited retries every five seconds. +Mutually exclusive with `recoveryInterval`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[recoveryInterval]]<> + +(recovery-interval) + +|Determines the time in milliseconds between attempts to start a consumer if it fails to start for non-fatal reasons. +Default: 5000. +Mutually exclusive with `recoveryBackOff`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[retryDeclarationInterval]]<> + +(missing-queue- +retry-interval) + +|If a subset of the configured queues are available during consumer initialization, the consumer starts consuming from those queues. +The consumer tries to passively declare the missing queues by using this interval. +When this interval elapses, the 'declarationRetries' and 'failedDeclarationRetryInterval' is used again. +If there are still missing queues, the consumer again waits for this interval before trying again. +This process continues indefinitely until all queues are available. +Default: 60000 (one minute). + +a|image::tickmark.png[] +a| +a| + +|[[shutdownTimeout]]<> + +(N/A) + +|When a container shuts down (for example, +if its enclosing `ApplicationContext` is closed), it waits for in-flight messages to be processed up to this limit. +Defaults to five seconds. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[startConsumerMinInterval]]<> + +(min-start-interval) + +|The time in milliseconds that must elapse before each new consumer is started on demand. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. +Default: 10000 (10 seconds). + +a|image::tickmark.png[] +a| +a| + +|[[statefulRetryFatal]]<> + +WithNullMessageId +(N/A) + +|When using a stateful retry advice, if a message with a missing `messageId` property is received, it is considered +fatal for the consumer (it is stopped) by default. +Set this to `false` to discard (or route to a dead-letter queue) such messages. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[stopConsumerMinInterval]]<> + +(min-stop-interval) + +|The time in milliseconds that must elapse before a consumer is stopped since the last consumer was stopped when an idle consumer is detected. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. +Default: 60000 (one minute). + +a|image::tickmark.png[] +a| +a| + +|[[streamConverter]]<> + +(N/A) + +|A `StreamMessageConverter` to convert a native Stream message to a Spring AMQP message. + +a| +a| +a|image::tickmark.png[] + +|[[taskExecutor]]<> + +(task-executor) + +|A reference to a Spring `TaskExecutor` (or standard JDK 1.5+ `Executor`) for executing listener invokers. +Default is a `SimpleAsyncTaskExecutor`, using internally managed threads. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[taskScheduler]]<> + +(task-scheduler) + +|With the DMLC, the scheduler used to run the monitor task at the 'monitorInterval'. + +a| +a|image::tickmark.png[] +a| + +|[[transactionManager]]<> + +(transaction-manager) + +|External transaction manager for the operation of the listener. +Also complementary to `channelTransacted` -- if the `Channel` is transacted, its transaction is synchronized with the external transaction. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| +|=== + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/containers-and-broker-named-queues.adoc b/src/reference/antora/modules/ROOT/pages/amqp/containers-and-broker-named-queues.adoc new file mode 100644 index 0000000000..4ca70426e2 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/containers-and-broker-named-queues.adoc @@ -0,0 +1,35 @@ +[[containers-and-broker-named-queues]] += Containers and Broker-Named queues + +While it is preferable to use `AnonymousQueue` instances as auto-delete queues, starting with version 2.1, you can use broker named queues with listener containers. +The following example shows how to do so: + +[source, java] +---- +@Bean +public Queue queue() { + return new Queue("", false, true, true); +} + +@Bean +public SimpleMessageListenerContainer container() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf()); + container.setQueues(queue()); + container.setMessageListener(m -> { + ... + }); + container.setMissingQueuesFatal(false); + return container; +} +---- + +Notice the empty `String` for the name. +When the `RabbitAdmin` declares queues, it updates the `Queue.actualName` property with the name returned by the broker. +You must use `setQueues()` when you configure the container for this to work, so that the container can access the declared name at runtime. +Just setting the names is insufficient. + +NOTE: You cannot add broker-named queues to the containers while they are running. + +IMPORTANT: When a connection is reset and a new one is established, the new queue gets a new name. +Since there is a race condition between the container restarting and the queue being re-declared, it is important to set the container's `missingQueuesFatal` property to `false`, since the container is likely to initially try to reconnect to the old queue. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/custom-client-props.adoc b/src/reference/antora/modules/ROOT/pages/amqp/custom-client-props.adoc new file mode 100644 index 0000000000..e80e4974bd --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/custom-client-props.adoc @@ -0,0 +1,15 @@ +[[custom-client-props]] += Adding Custom Client Connection Properties +:page-section-summary-toc: 1 + +The `CachingConnectionFactory` now lets you access the underlying connection factory to allow, for example, +setting custom client properties. +The following example shows how to do so: + +[source, java] +---- +connectionFactory.getRabbitConnectionFactory().getClientProperties().put("thing1", "thing2"); +---- + +These properties appear in the RabbitMQ Admin UI when viewing the connection. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/debugging.adoc b/src/reference/antora/modules/ROOT/pages/amqp/debugging.adoc new file mode 100644 index 0000000000..5ea5ccef21 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/debugging.adoc @@ -0,0 +1,12 @@ +[[debugging]] += Debugging +:page-section-summary-toc: 1 + +Spring AMQP provides extensive logging, especially at the `DEBUG` level. + +If you wish to monitor the AMQP protocol between the application and broker, you can use a tool such as WireShark, which has a plugin to decode the protocol. +Alternatively, the RabbitMQ Java client comes with a very useful class called `Tracer`. +When run as a `main`, by default, it listens on port 5673 and connects to port 5672 on localhost. +You can run it and change your connection factory configuration to connect to port 5673 on localhost. +It displays the decoded protocol on the console. +Refer to the `Tracer` Javadoc for more information. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/delayed-message-exchange.adoc b/src/reference/antora/modules/ROOT/pages/amqp/delayed-message-exchange.adoc new file mode 100644 index 0000000000..5593e3b475 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/delayed-message-exchange.adoc @@ -0,0 +1,51 @@ +[[delayed-message-exchange]] += Delayed Message Exchange + +Version 1.6 introduces support for the +https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq/[Delayed Message Exchange Plugin] + +NOTE: The plugin is currently marked as experimental but has been available for over a year (at the time of writing). +If changes to the plugin make it necessary, we plan to add support for such changes as soon as practical. +For that reason, this support in Spring AMQP should be considered experimental, too. +This functionality was tested with RabbitMQ 3.6.0 and version 0.0.1 of the plugin. + +To use a `RabbitAdmin` to declare an exchange as delayed, you can set the `delayed` property on the exchange bean to +`true`. +The `RabbitAdmin` uses the exchange type (`Direct`, `Fanout`, and so on) to set the `x-delayed-type` argument and +declare the exchange with type `x-delayed-message`. + +The `delayed` property (default: `false`) is also available when configuring exchange beans using XML. +The following example shows how to use it: + +[source, xml] +---- + +---- + +To send a delayed message, you can set the `x-delay` header through `MessageProperties`, as the following examples show: + +[source, java] +---- +MessageProperties properties = new MessageProperties(); +properties.setDelay(15000); +template.send(exchange, routingKey, + MessageBuilder.withBody("foo".getBytes()).andProperties(properties).build()); +---- + +[source, java] +---- +rabbitTemplate.convertAndSend(exchange, routingKey, "foo", new MessagePostProcessor() { + + @Override + public Message postProcessMessage(Message message) throws AmqpException { + message.getMessageProperties().setDelay(15000); + return message; + } + +}); +---- + +To check if a message was delayed, use the `getReceivedDelay()` method on the `MessageProperties`. +It is a separate property to avoid unintended propagation to an output message generated from an input message. + + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/exception-handling.adoc b/src/reference/antora/modules/ROOT/pages/amqp/exception-handling.adoc new file mode 100644 index 0000000000..9658b72b57 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/exception-handling.adoc @@ -0,0 +1,46 @@ +[[exception-handling]] += Exception Handling + +Many operations with the RabbitMQ Java client can throw checked exceptions. +For example, there are a lot of cases where `IOException` instances may be thrown. +The `RabbitTemplate`, `SimpleMessageListenerContainer`, and other Spring AMQP components catch those exceptions and convert them into one of the exceptions within `AmqpException` hierarchy. +Those are defined in the 'org.springframework.amqp' package, and `AmqpException` is the base of the hierarchy. + +When a listener throws an exception, it is wrapped in a `ListenerExecutionFailedException`. +Normally the message is rejected and requeued by the broker. +Setting `defaultRequeueRejected` to `false` causes messages to be discarded (or routed to a dead letter exchange). +As discussed in xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case], the listener can throw an `AmqpRejectAndDontRequeueException` (or `ImmediateRequeueAmqpException`) to conditionally control this behavior. + +However, there is a class of errors where the listener cannot control the behavior. +When a message that cannot be converted is encountered (for example, an invalid `content_encoding` header), some exceptions are thrown before the message reaches user code. +With `defaultRequeueRejected` set to `true` (default) (or throwing an `ImmediateRequeueAmqpException`), such messages would be redelivered over and over. +Before version 1.3.2, users needed to write a custom `ErrorHandler`, as discussed in xref:amqp/exception-handling.adoc[Exception Handling], to avoid this situation. + +Starting with version 1.3.2, the default `ErrorHandler` is now a `ConditionalRejectingErrorHandler` that rejects (and does not requeue) messages that fail with an irrecoverable error. +Specifically, it rejects messages that fail with the following errors: + +* `o.s.amqp...MessageConversionException`: Can be thrown when converting the incoming message payload using a `MessageConverter`. +* `o.s.messaging...MessageConversionException`: Can be thrown by the conversion service if additional conversion is required when mapping to a `@RabbitListener` method. +* `o.s.messaging...MethodArgumentNotValidException`: Can be thrown if validation (for example, `@Valid`) is used in the listener and the validation fails. +* `o.s.messaging...MethodArgumentTypeMismatchException`: Can be thrown if the inbound message was converted to a type that is not correct for the target method. +For example, the parameter is declared as `Message` but `Message` is received. +* `java.lang.NoSuchMethodException`: Added in version 1.6.3. +* `java.lang.ClassCastException`: Added in version 1.6.3. + +You can configure an instance of this error handler with a `FatalExceptionStrategy` so that users can provide their own rules for conditional message rejection -- for example, a delegate implementation to the `BinaryExceptionClassifier` from Spring Retry (xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case]). +In addition, the `ListenerExecutionFailedException` now has a `failedMessage` property that you can use in the decision. +If the `FatalExceptionStrategy.isFatal()` method returns `true`, the error handler throws an `AmqpRejectAndDontRequeueException`. +The default `FatalExceptionStrategy` logs a warning message when an exception is determined to be fatal. + +Since version 1.6.3, a convenient way to add user exceptions to the fatal list is to subclass `ConditionalRejectingErrorHandler.DefaultExceptionStrategy` and override the `isUserCauseFatal(Throwable cause)` method to return `true` for fatal exceptions. + +A common pattern for handling DLQ messages is to set a `time-to-live` on those messages as well as additional DLQ configuration such that these messages expire and are routed back to the main queue for retry. +The problem with this technique is that messages that cause fatal exceptions loop forever. +Starting with version 2.1, the `ConditionalRejectingErrorHandler` detects an `x-death` header on a message that causes a fatal exception to be thrown. +The message is logged and discarded. +You can revert to the previous behavior by setting the `discardFatalsWithXDeath` property on the `ConditionalRejectingErrorHandler` to `false`. + +IMPORTANT: Starting with version 2.1.9, messages with these fatal exceptions are rejected and NOT requeued by default, even if the container acknowledge mode is MANUAL. +These exceptions generally occur before the listener is invoked so the listener does not have a chance to ack or nack the message so it remained in the queue in an un-acked state. +To revert to the previous behavior, set the `rejectManual` property on the `ConditionalRejectingErrorHandler` to `false`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/exclusive-consumer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/exclusive-consumer.adoc new file mode 100644 index 0000000000..23c1a36a39 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/exclusive-consumer.adoc @@ -0,0 +1,10 @@ +[[exclusive-consumer]] += Exclusive Consumer +:page-section-summary-toc: 1 + +Starting with version 1.3, you can configure the listener container with a single exclusive consumer. +This prevents other containers from consuming from the queues until the current consumer is cancelled. +The concurrency of such a container must be `1`. + +When using exclusive consumers, other containers try to consume from the queues according to the `recoveryInterval` property and log a `WARN` message if the attempt fails. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/listener-concurrency.adoc b/src/reference/antora/modules/ROOT/pages/amqp/listener-concurrency.adoc new file mode 100644 index 0000000000..8a76771a52 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/listener-concurrency.adoc @@ -0,0 +1,46 @@ +[[listener-concurrency]] += Listener Concurrency + +[[simplemessagelistenercontainer]] +== SimpleMessageListenerContainer + +By default, the listener container starts a single consumer that receives messages from the queues. + +When examining the table in the previous section, you can see a number of properties and attributes that control concurrency. +The simplest is `concurrentConsumers`, which creates that (fixed) number of consumers that concurrently process messages. + +Prior to version 1.3.0, this was the only setting available and the container had to be stopped and started again to change the setting. + +Since version 1.3.0, you can now dynamically adjust the `concurrentConsumers` property. +If it is changed while the container is running, consumers are added or removed as necessary to adjust to the new setting. + +In addition, a new property called `maxConcurrentConsumers` has been added and the container dynamically adjusts the concurrency based on workload. +This works in conjunction with four additional properties: `consecutiveActiveTrigger`, `startConsumerMinInterval`, `consecutiveIdleTrigger`, and `stopConsumerMinInterval`. +With the default settings, the algorithm to increase consumers works as follows: + +If the `maxConcurrentConsumers` has not been reached and an existing consumer is active for ten consecutive cycles AND at least 10 seconds has elapsed since the last consumer was started, a new consumer is started. +A consumer is considered active if it received at least one message in `batchSize` * `receiveTimeout` milliseconds. + +With the default settings, the algorithm to decrease consumers works as follows: + +If there are more than `concurrentConsumers` running and a consumer detects ten consecutive timeouts (idle) AND the last consumer was stopped at least 60 seconds ago, a consumer is stopped. +The timeout depends on the `receiveTimeout` and the `batchSize` properties. +A consumer is considered idle if it receives no messages in `batchSize` * `receiveTimeout` milliseconds. +So, with the default timeout (one second) and a `batchSize` of four, stopping a consumer is considered after 40 seconds of idle time (four timeouts correspond to one idle detection). + +NOTE: Practically, consumers can be stopped only if the whole container is idle for some time. +This is because the broker shares its work across all the active consumers. + +Each consumer uses a single channel, regardless of the number of configured queues. + +Starting with version 2.0, the `concurrentConsumers` and `maxConcurrentConsumers` properties can be set with the `concurrency` property -- for example, `2-4`. + +[[using-directmessagelistenercontainer]] +== Using `DirectMessageListenerContainer` + +With this container, concurrency is based on the configured queues and `consumersPerQueue`. +Each consumer for each queue uses a separate channel, and the concurrency is controlled by the rabbit client library. +By default, at the time of writing, it uses a pool of `DEFAULT_NUM_THREADS = Runtime.getRuntime().availableProcessors() * 2` threads. + +You can configure a `taskExecutor` to provide the required maximum concurrency. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc new file mode 100644 index 0000000000..c9972b3688 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc @@ -0,0 +1,19 @@ +[[listener-queues]] += Listener Container Queues +:page-section-summary-toc: 1 + +Version 1.3 introduced a number of improvements for handling multiple queues in a listener container. + +Container can be initially configured to listen on zero queues. +Queues can be added and removed at runtime. +The `SimpleMessageListenerContainer` recycles (cancels and re-creates) all consumers when any pre-fetched messages have been processed. +The `DirectMessageListenerContainer` creates/cancels individual consumer(s) for each queue without affecting consumers on other queues. +See the javadoc:org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. + +If not all queues are available, the container tries to passively declare (and consume from) the missing queues every 60 seconds. + +Also, if a consumer receives a cancel from the broker (for example, if a queue is deleted) the consumer tries to recover, and the recovered consumer continues to process messages from any other configured queues. +Previously, a cancel on one queue cancelled the entire consumer and, eventually, the container would stop due to the missing queue. + +If you wish to permanently remove a queue, you should update the container before or after deleting to queue, to avoid future attempts trying to consume from it. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc new file mode 100644 index 0000000000..00d31891b0 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc @@ -0,0 +1,39 @@ +[[management-rest-api]] += RabbitMQ REST API +:page-section-summary-toc: 1 + +When the management plugin is enabled, the RabbitMQ server exposes a REST API to monitor and configure the broker. +A {rabbitmq-github}/hop[Java Binding for the API] is now provided. +The `com.rabbitmq.http.client.Client` is a standard, immediate, and, therefore, blocking API. +It is based on the {spring-framework-docs}/web.html[Spring Web] module and its `RestTemplate` implementation. +On the other hand, the `com.rabbitmq.http.client.ReactorNettyClient` is a reactive, non-blocking implementation based on the https://projectreactor.io/docs/netty/release/reference/docs/index.html[Reactor Netty] project. + +Also, the https://www.rabbitmq.com/docs/management#http-api-endpoints[management REST API] can be used with any HTTP client. +The next example demonstrates how to get a queue information using {spring-framework-docs}/web/webflux-webclient.html[WebClient]: + +[source,java] +---- + public Map queueInfo(String queueName) throws URISyntaxException { + WebClient client = createClient("admin", "admin"); + URI uri = queueUri(queueName); + return client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + } + + private URI queueUri(String queue) throws URISyntaxException { + URI uri = new URI("http://localhost:15672/api/") + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queue); + return uri; + } + + private WebClient createClient(String adminUser, String adminPassword) { + return WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(adminUser, adminPassword)) + .build(); + } +---- diff --git a/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc b/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc new file mode 100644 index 0000000000..c028af61bd --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc @@ -0,0 +1,513 @@ +[[message-converters]] += Message Converters + +The `AmqpTemplate` also defines several methods for sending and receiving messages that delegate to a `MessageConverter`. +The `MessageConverter` provides a single method for each direction: one for converting *to* a `Message` and another for converting *from* a `Message`. +Notice that, when converting to a `Message`, you can also provide properties in addition to the object. +The `object` parameter typically corresponds to the Message body. +The following listing shows the `MessageConverter` interface definition: + +[source,java] +---- +public interface MessageConverter { + + Message toMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException; + + Object fromMessage(Message message) throws MessageConversionException; + +} +---- + +The relevant `Message`-sending methods on the `AmqpTemplate` are simpler than the methods we discussed previously, because they do not require the `Message` instance. +Instead, the `MessageConverter` is responsible for "`creating`" each `Message` by converting the provided object to the byte array for the `Message` body and then adding any provided `MessageProperties`. +The following listing shows the definitions of the various methods: + +[source,java] +---- +void convertAndSend(Object message) throws AmqpException; + +void convertAndSend(String routingKey, Object message) throws AmqpException; + +void convertAndSend(String exchange, String routingKey, Object message) + throws AmqpException; + +void convertAndSend(Object message, MessagePostProcessor messagePostProcessor) + throws AmqpException; + +void convertAndSend(String routingKey, Object message, + MessagePostProcessor messagePostProcessor) throws AmqpException; + +void convertAndSend(String exchange, String routingKey, Object message, + MessagePostProcessor messagePostProcessor) throws AmqpException; +---- + +On the receiving side, there are only two methods: one that accepts the queue name and one that relies on the template's "`queue`" property having been set. +The following listing shows the definitions of the two methods: + +[source,java] +---- +Object receiveAndConvert() throws AmqpException; + +Object receiveAndConvert(String queueName) throws AmqpException; +---- + +NOTE: The `MessageListenerAdapter` mentioned in xref:amqp/receiving-messages/async-consumer.adoc[Asynchronous Consumer] also uses a `MessageConverter`. + +[[simple-message-converter]] +== `SimpleMessageConverter` + +The default implementation of the `MessageConverter` strategy is called `SimpleMessageConverter`. +This is the converter that is used by an instance of `RabbitTemplate` if you do not explicitly configure an alternative. +It handles text-based content, serialized Java objects, and byte arrays. + +[[converting-from-a-message]] +=== Converting From a `Message` + +If the content type of the input `Message` begins with "text" (for example, +"text/plain"), it also checks for the content-encoding property to determine the charset to be used when converting the `Message` body byte array to a Java `String`. +If no content-encoding property had been set on the input `Message`, it uses the UTF-8 charset by default. +If you need to override that default setting, you can configure an instance of `SimpleMessageConverter`, set its `defaultCharset` property, and inject that into a `RabbitTemplate` instance. + +If the content-type property value of the input `Message` is set to "application/x-java-serialized-object", the `SimpleMessageConverter` tries to deserialize (rehydrate) the byte array into a Java object. +While that might be useful for simple prototyping, we do not recommend relying on Java serialization, since it leads to tight coupling between the producer and the consumer. +Of course, it also rules out usage of non-Java systems on either side. +With AMQP being a wire-level protocol, it would be unfortunate to lose much of that advantage with such restrictions. +In the next two sections, we explore some alternatives for passing rich domain object content without relying on Java serialization. + +For all other content-types, the `SimpleMessageConverter` returns the `Message` body content directly as a byte array. + +See <> for important information. + +[[converting-to-a-message]] +=== Converting To a `Message` + +When converting to a `Message` from an arbitrary Java Object, the `SimpleMessageConverter` likewise deals with byte arrays, strings, and serializable instances. +It converts each of these to bytes (in the case of byte arrays, there is nothing to convert), and it sets the content-type property accordingly. +If the `Object` to be converted does not match one of those types, the `Message` body is null. + +[[serializer-message-converter]] +== `SerializerMessageConverter` + +This converter is similar to the `SimpleMessageConverter` except that it can be configured with other Spring Framework +`Serializer` and `Deserializer` implementations for `application/x-java-serialized-object` conversions. + +See <> for important information. + +[[json-message-converter]] +== Jackson2JsonMessageConverter + +This section covers using the `Jackson2JsonMessageConverter` to convert to and from a `Message`. +It has the following sections: + +* xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-to-message[Converting to a `Message`] +* xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-from-message[Converting from a `Message`] + +[[Jackson2JsonMessageConverter-to-message]] +=== Converting to a `Message` + +As mentioned in the previous section, relying on Java serialization is generally not recommended. +One rather common alternative that is more flexible and portable across different languages and platforms is JSON +(JavaScript Object Notation). +The converter can be configured on any `RabbitTemplate` instance to override its usage of the `SimpleMessageConverter` +default. +The `Jackson2JsonMessageConverter` uses the `com.fasterxml.jackson` 2.x library. +The following example configures a `Jackson2JsonMessageConverter`: + +[source,xml] +---- + + + + + + + + + +---- + +As shown above, `Jackson2JsonMessageConverter` uses a `DefaultClassMapper` by default. +Type information is added to (and retrieved from) `MessageProperties`. +If an inbound message does not contain type information in `MessageProperties`, but you know the expected type, you +can configure a static type by using the `defaultType` property, as the following example shows: + +[source,xml] +---- + + + + + + + +---- + +In addition, you can provide custom mappings from the value in the `__TypeId__` header. +The following example shows how to do so: + +[source, java] +---- +@Bean +public Jackson2JsonMessageConverter jsonMessageConverter() { + Jackson2JsonMessageConverter jsonConverter = new Jackson2JsonMessageConverter(); + jsonConverter.setClassMapper(classMapper()); + return jsonConverter; +} + +@Bean +public DefaultClassMapper classMapper() { + DefaultClassMapper classMapper = new DefaultClassMapper(); + Map> idClassMapping = new HashMap<>(); + idClassMapping.put("thing1", Thing1.class); + idClassMapping.put("thing2", Thing2.class); + classMapper.setIdClassMapping(idClassMapping); + return classMapper; +} +---- + +Now, if the sending system sets the header to `thing1`, the converter creates a `Thing1` object, and so on. +See the xref:sample-apps.adoc#spring-rabbit-json[Receiving JSON from Non-Spring Applications] sample application for a complete discussion about converting messages from non-Spring applications. + +Starting with version 2.4.3, the converter will not add a `contentEncoding` message property if the `supportedMediaType` has a `charset` parameter; this is also used for the encoding. +A new method `setSupportedMediaType` has been added: + +[source, java] +---- +String utf16 = "application/json; charset=utf-16"; +converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); +---- + +[[Jackson2JsonMessageConverter-from-message]] +=== Converting from a `Message` + +Inbound messages are converted to objects according to the type information added to headers by the sending system. + +Starting with version 2.4.3, if there is no `contentEncoding` message property, the converter will attempt to detect a `charset` parameter in the `contentType` message property and use that. +If neither exist, if the `supportedMediaType` has a `charset` parameter, it will be used for decoding, with a final fallback to the `defaultCharset` property. +A new method `setSupportedMediaType` has been added: + +[source, java] +---- +String utf16 = "application/json; charset=utf-16"; +converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); +---- + +In versions prior to 1.6, if type information is not present, conversion would fail. +Starting with version 1.6, if type information is missing, the converter converts the JSON by using Jackson defaults (usually a map). + +Also, starting with version 1.6, when you use `@RabbitListener` annotations (on methods), the inferred type information is added to the `MessageProperties`. +This lets the converter convert to the argument type of the target method. +This only applies if there is one parameter with no annotations or a single parameter with the `@Payload` annotation. +Parameters of type `Message` are ignored during the analysis. + +IMPORTANT: By default, the inferred type information will override the inbound `__TypeId__` and related headers created +by the sending system. +This lets the receiving system automatically convert to a different domain object. +This applies only if the parameter type is concrete (not abstract or an interface) or it is from the `java.util` +package. +In all other cases, the `__TypeId__` and related headers is used. +There are cases where you might wish to override the default behavior and always use the `__TypeId__` information. +For example, suppose you have a `@RabbitListener` that takes a `Thing1` argument but the message contains a `Thing2` that +is a subclass of `Thing1` (which is concrete). +The inferred type would be incorrect. +To handle this situation, set the `TypePrecedence` property on the `Jackson2JsonMessageConverter` to `TYPE_ID` instead +of the default `INFERRED`. +(The property is actually on the converter's `DefaultJackson2JavaTypeMapper`, but a setter is provided on the converter +for convenience.) +If you inject a custom type mapper, you should set the property on the mapper instead. + +NOTE: When converting from the `Message`, an incoming `MessageProperties.getContentType()` must be JSON-compliant (`contentType.contains("json")` is used to check). +Starting with version 2.2, `application/json` is assumed if there is no `contentType` property, or it has the default value `application/octet-stream`. +To revert to the previous behavior (return an unconverted `byte[]`), set the converter's `assumeSupportedContentType` property to `false`. +If the content type is not supported, a `WARN` log message `Could not convert incoming message with content-type [...]`, is emitted and `message.getBody()` is returned as is -- as a `byte[]`. +So, to meet the `Jackson2JsonMessageConverter` requirements on the consumer side, the producer must add the `contentType` message property -- for example, as `application/json` or `text/x-json` or by using the `Jackson2JsonMessageConverter`, which sets the header automatically. +The following listing shows a number of converter calls: + +[source, java] +---- +@RabbitListener +public void thing1(Thing1 thing1) {...} + +@RabbitListener +public void thing1(@Payload Thing1 thing1, @Header("amqp_consumerQueue") String queue) {...} + +@RabbitListener +public void thing1(Thing1 thing1, o.s.amqp.core.Message message) {...} + +@RabbitListener +public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} + +@RabbitListener +public void thing1(Thing1 thing1, String bar) {...} + +@RabbitListener +public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} +---- + +In the first four cases in the preceding listing, the converter tries to convert to the `Thing1` type. +The fifth example is invalid because we cannot determine which argument should receive the message payload. +With the sixth example, the Jackson defaults apply due to the generic type being a `WildcardType`. + +You can, however, create a custom converter and use the `targetMethod` message property to decide which type to convert +the JSON to. + +NOTE: This type inference can only be achieved when the `@RabbitListener` annotation is declared at the method level. +With class-level `@RabbitListener`, the converted type is used to select which `@RabbitHandler` method to invoke. +For this reason, the infrastructure provides the `targetObject` message property, which you can use in a custom +converter to determine the type. + +IMPORTANT: Starting with version 1.6.11, `Jackson2JsonMessageConverter` and, therefore, `DefaultJackson2JavaTypeMapper` (`DefaultClassMapper`) provide the `trustedPackages` option to overcome https://pivotal.io/security/cve-2017-4995[Serialization Gadgets] vulnerability. +By default and for backward compatibility, the `Jackson2JsonMessageConverter` trusts all packages -- that is, it uses `*` for the option. + +Starting with version 2.4.7, the converter can be configured to return `Optional.empty()` if Jackson returns `null` after deserializing the message body. +This facilitates `@RabbitListener` s to receive null payloads, in two ways: + +[source, java] +---- +@RabbitListener(queues = "op.1") +void listen(@Payload(required = false) Thing payload) { + handleOptional(payload); // payload might be null +} + +@RabbitListener(queues = "op.2") +void listen(Optional optional) { + handleOptional(optional.orElse(this.emptyThing)); +} +---- + +To enable this feature, set `setNullAsOptionalEmpty` to `true`; when `false` (default), the converter falls back to the raw message body (`byte[]`). + +[source, java] +---- +@Bean +Jackson2JsonMessageConverter converter() { + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + converter.setNullAsOptionalEmpty(true); + return converter; +} +---- + +[[jackson-abstract]] +=== Deserializing Abstract Classes + +Prior to version 2.2.8, if the inferred type of a `@RabbitListener` was an abstract class (including interfaces), the converter would fall back to looking for type information in the headers and, if present, used that information; if that was not present, it would try to create the abstract class. +This caused a problem when a custom `ObjectMapper` that is configured with a custom deserializer to handle the abstract class is used, but the incoming message has invalid type headers. + +Starting with version 2.2.8, the previous behavior is retained by default. If you have such a custom `ObjectMapper` and you want to ignore type headers, and always use the inferred type for conversion, set the `alwaysConvertToInferredType` to `true`. +This is needed for backwards compatibility and to avoid the overhead of an attempted conversion when it would fail (with a standard `ObjectMapper`). + +[[data-projection]] +=== Using Spring Data Projection Interfaces + +Starting with version 2.2, you can convert JSON to a Spring Data Projection interface instead of a concrete type. +This allows very selective, and low-coupled bindings to data, including the lookup of values from multiple places inside the JSON document. +For example the following interface can be defined as message payload type: + +[source, java] +---- +interface SomeSample { + + @JsonPath({ "$.username", "$.user.name" }) + String getUsername(); + +} +---- + +[source, java] +---- +@RabbitListener(queues = "projection") +public void projection(SomeSample in) { + String username = in.getUsername(); + ... +} +---- + +Accessor methods will be used to lookup the property name as field in the received JSON document by default. +The `@JsonPath` expression allows customization of the value lookup, and even to define multiple JSON path expressions, to lookup values from multiple places until an expression returns an actual value. + +To enable this feature, set the `useProjectionForInterfaces` to `true` on the message converter. +You must also add `spring-data:spring-data-commons` and `com.jayway.jsonpath:json-path` to the class path. + +When used as the parameter to a `@RabbitListener` method, the interface type is automatically passed to the converter as normal. + +[[json-complex]] +=== Converting From a `Message` With `RabbitTemplate` + +As mentioned earlier, type information is conveyed in message headers to assist the converter when converting from a message. +This works fine in most cases. +However, when using generic types, it can only convert simple objects and known "`container`" objects (lists, arrays, and maps). +Starting with version 2.0, the `Jackson2JsonMessageConverter` implements `SmartMessageConverter`, which lets it be used with the new `RabbitTemplate` methods that take a `ParameterizedTypeReference` argument. +This allows conversion of complex generic types, as shown in the following example: + +[source, java] +---- +Thing1> thing1 = + rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference>>() { }); +---- + +NOTE: Starting with version 2.1, the `AbstractJsonMessageConverter` class has been removed. +It is no longer the base class for `Jackson2JsonMessageConverter`. +It has been replaced by `AbstractJackson2MessageConverter`. + +[[marshallingmessageconverter]] +== `MarshallingMessageConverter` + +Yet another option is the `MarshallingMessageConverter`. +It delegates to the Spring OXM library's implementations of the `Marshaller` and `Unmarshaller` strategy interfaces. +You can read more about that library {spring-framework-docs}/data-access/oxm.html[here]. +In terms of configuration, it is most common to provide only the constructor argument, since most implementations of `Marshaller` also implement `Unmarshaller`. +The following example shows how to configure a `MarshallingMessageConverter`: + +[source,xml] +---- + + + + + + + + +---- + +[[jackson2xml]] +== `Jackson2XmlMessageConverter` + +This class was introduced in version 2.1 and can be used to convert messages from and to XML. + +Both `Jackson2XmlMessageConverter` and `Jackson2JsonMessageConverter` have the same base class: `AbstractJackson2MessageConverter`. + +NOTE: The `AbstractJackson2MessageConverter` class is introduced to replace a removed class: `AbstractJsonMessageConverter`. + +The `Jackson2XmlMessageConverter` uses the `com.fasterxml.jackson` 2.x library. + +You can use it the same way as `Jackson2JsonMessageConverter`, except it supports XML instead of JSON. +The following example configures a `Jackson2JsonMessageConverter`: + +[source,xml] +---- + + + + + + + +---- +See <> for more information. + +NOTE: Starting with version 2.2, `application/xml` is assumed if there is no `contentType` property, or it has the default value `application/octet-stream`. +To revert to the previous behavior (return an unconverted `byte[]`), set the converter's `assumeSupportedContentType` property to `false`. + +[[contenttypedelegatingmessageconverter]] +== `ContentTypeDelegatingMessageConverter` + +This class was introduced in version 1.4.2 and allows delegation to a specific `MessageConverter` based on the content type property in the `MessageProperties`. +By default, it delegates to a `SimpleMessageConverter` if there is no `contentType` property or there is a value that matches none of the configured converters. +The following example configures a `ContentTypeDelegatingMessageConverter`: + +[source,xml] +---- + + + + + + + + +---- + +[[java-deserialization]] +== Java Deserialization + +This section covers how to deserialize Java objects. + +[IMPORTANT] +==== +There is a possible vulnerability when deserializing java objects from untrusted sources. + +If you accept messages from untrusted sources with a `content-type` of `application/x-java-serialized-object`, you should +consider configuring which packages and classes are allowed to be deserialized. +This applies to both the `SimpleMessageConverter` and `SerializerMessageConverter` when it is configured to use a +`DefaultDeserializer` either implicitly or via configuration. + +By default, the allowed list is empty, meaning no classes will be deserialized. + +You can set a list of patterns, such as `thing1.*`, `thing1.thing2.Cat` or `*.MySafeClass`. + +The patterns are checked in order until a match is found. +If there is no match, a `SecurityException` is thrown. + +You can set the patterns using the `allowedListPatterns` property on these converters. +Alternatively, if you trust all message originators, you can set the environment variable `SPRING_AMQP_DESERIALIZATION_TRUST_ALL` or system property `spring.amqp.deserialization.trust.all` to `true`. +==== + +[[message-properties-converters]] +== Message Properties Converters + +The `MessagePropertiesConverter` strategy interface is used to convert between the Rabbit Client `BasicProperties` and Spring AMQP `MessageProperties`. +The default implementation (`DefaultMessagePropertiesConverter`) is usually sufficient for most purposes, but you can implement your own if needed. +The default properties converter converts `BasicProperties` elements of type `LongString` to `String` instances when the size is not greater than `1024` bytes. +Larger `LongString` instances are not converted (see the next paragraph). +This limit can be overridden with a constructor argument. + +Starting with version 1.6, headers longer than the long string limit (default: 1024) are now left as +`LongString` instances by default by the `DefaultMessagePropertiesConverter`. +You can access the contents through the `getBytes[]`, `toString()`, or `getStream()` methods. + +Previously, the `DefaultMessagePropertiesConverter` "`converted`" such headers to a `DataInputStream` (actually it just referenced the `LongString` instance's `DataInputStream`). +On output, this header was not converted (except to a String -- for example, `java.io.DataInputStream@1d057a39` by calling `toString()` on the stream). + +Large incoming `LongString` headers are now correctly "`converted`" on output, too (by default). + +A new constructor is provided to let you configure the converter to work as before. +The following listing shows the Javadoc comment and declaration of the method: + +[source, java] +---- +/** + * Construct an instance where LongStrings will be returned + * unconverted or as a java.io.DataInputStream when longer than this limit. + * Use this constructor with 'true' to restore pre-1.6 behavior. + * @param longStringLimit the limit. + * @param convertLongLongStrings LongString when false, + * DataInputStream when true. + * @since 1.6 + */ +public DefaultMessagePropertiesConverter(int longStringLimit, boolean convertLongLongStrings) { ... } +---- + +Also starting with version 1.6, a new property called `correlationIdString` has been added to `MessageProperties`. +Previously, when converting to and from `BasicProperties` used by the RabbitMQ client, an unnecessary `byte[] <-> String` conversion was performed because `MessageProperties.correlationId` is a `byte[]`, but `BasicProperties` uses a `String`. +(Ultimately, the RabbitMQ client uses UTF-8 to convert the `String` to bytes to put in the protocol message). + +To provide maximum backwards compatibility, a new property called `correlationIdPolicy` has been added to the +`DefaultMessagePropertiesConverter`. +This takes a `DefaultMessagePropertiesConverter.CorrelationIdPolicy` enum argument. +By default it is set to `BYTES`, which replicates the previous behavior. + +For inbound messages: + +* `STRING`: Only the `correlationIdString` property is mapped +* `BYTES`: Only the `correlationId` property is mapped +* `BOTH`: Both properties are mapped + +For outbound messages: + +* `STRING`: Only the `correlationIdString` property is mapped +* `BYTES`: Only the `correlationId` property is mapped +* `BOTH`: Both properties are considered, with the `String` property taking precedence + +Also starting with version 1.6, the inbound `deliveryMode` property is no longer mapped to `MessageProperties.deliveryMode`. +It is mapped to `MessageProperties.receivedDeliveryMode` instead. +Also, the inbound `userId` property is no longer mapped to `MessageProperties.userId`. +It is mapped to `MessageProperties.receivedUserId` instead. +These changes are to avoid unexpected propagation of these properties if the same `MessageProperties` object is used for an outbound message. + +Starting with version 2.2, the `DefaultMessagePropertiesConverter` converts any custom headers with values of type `Class` using `getName()` instead of `toString()`; this avoids consuming application having to parse the class name out of the `toString()` representation. +For rolling upgrades, you may need to change your consumers to understand both formats until all producers are upgraded. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc b/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc new file mode 100644 index 0000000000..420fa05d44 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc @@ -0,0 +1,149 @@ +[[multi-rabbit]] += Multiple Broker (or Cluster) Support + +Version 2.3 added more convenience when communicating between a single application and multiple brokers or broker clusters. +The main benefit, on the consumer side, is that the infrastructure can automatically associate auto-declared queues with the appropriate broker. + +This is best illustrated with an example: + +[source, java] +---- +@SpringBootApplication(exclude = RabbitAutoConfiguration.class) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + CachingConnectionFactory cf1() { + return new CachingConnectionFactory("localhost"); + } + + @Bean + CachingConnectionFactory cf2() { + return new CachingConnectionFactory("otherHost"); + } + + @Bean + CachingConnectionFactory cf3() { + return new CachingConnectionFactory("thirdHost"); + } + + @Bean + SimpleRoutingConnectionFactory rcf(CachingConnectionFactory cf1, + CachingConnectionFactory cf2, CachingConnectionFactory cf3) { + + SimpleRoutingConnectionFactory rcf = new SimpleRoutingConnectionFactory(); + rcf.setDefaultTargetConnectionFactory(cf1); + rcf.setTargetConnectionFactories(Map.of("one", cf1, "two", cf2, "three", cf3)); + return rcf; + } + + @Bean("factory1-admin") + RabbitAdmin admin1(CachingConnectionFactory cf1) { + return new RabbitAdmin(cf1); + } + + @Bean("factory2-admin") + RabbitAdmin admin2(CachingConnectionFactory cf2) { + return new RabbitAdmin(cf2); + } + + @Bean("factory3-admin") + RabbitAdmin admin3(CachingConnectionFactory cf3) { + return new RabbitAdmin(cf3); + } + + @Bean + public RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry() { + return new RabbitListenerEndpointRegistry(); + } + + @Bean + public RabbitListenerAnnotationBeanPostProcessor postProcessor(RabbitListenerEndpointRegistry registry) { + MultiRabbitListenerAnnotationBeanPostProcessor postProcessor + = new MultiRabbitListenerAnnotationBeanPostProcessor(); + postProcessor.setEndpointRegistry(registry); + postProcessor.setContainerFactoryBeanName("defaultContainerFactory"); + return postProcessor; + } + + @Bean + public SimpleRabbitListenerContainerFactory factory1(CachingConnectionFactory cf1) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf1); + return factory; + } + + @Bean + public SimpleRabbitListenerContainerFactory factory2(CachingConnectionFactory cf2) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf2); + return factory; + } + + @Bean + public SimpleRabbitListenerContainerFactory factory3(CachingConnectionFactory cf3) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf3); + return factory; + } + + @Bean + RabbitTemplate template(SimpleRoutingConnectionFactory rcf) { + return new RabbitTemplate(rcf); + } + + @Bean + ConnectionFactoryContextWrapper wrapper(SimpleRoutingConnectionFactory rcf) { + return new ConnectionFactoryContextWrapper(rcf); + } + +} + +@Component +class Listeners { + + @RabbitListener(queuesToDeclare = @Queue("q1"), containerFactory = "factory1") + public void listen1(String in) { + + } + + @RabbitListener(queuesToDeclare = @Queue("q2"), containerFactory = "factory2") + public void listen2(String in) { + + } + + @RabbitListener(queuesToDeclare = @Queue("q3"), containerFactory = "factory3") + public void listen3(String in) { + + } + +} +---- + +As you can see, we have declared 3 sets of infrastructure (connection factories, admins, container factories). +As discussed earlier, `@RabbitListener` can define which container factory to use; in this case, they also use `queuesToDeclare` which causes the queue(s) to be declared on the broker, if it doesn't exist. +By naming the `RabbitAdmin` beans with the convention `-admin`, the infrastructure is able to determine which admin should declare the queue. +This will also work with `bindings = @QueueBinding(...)` whereby the exchange and binding will also be declared. +It will NOT work with `queues`, since that expects the queue(s) to already exist. + +On the producer side, a convenient `ConnectionFactoryContextWrapper` class is provided, to make using the `RoutingConnectionFactory` (see xref:amqp/connections.adoc#routing-connection-factory[Routing Connection Factory]) simpler. + +As you can see above, a `SimpleRoutingConnectionFactory` bean has been added with routing keys `one`, `two` and `three`. +There is also a `RabbitTemplate` that uses that factory. +Here is an example of using that template with the wrapper to route to one of the broker clusters. + +[source, java] +---- +@Bean +public ApplicationRunner runner(RabbitTemplate template, ConnectionFactoryContextWrapper wrapper) { + return args -> { + wrapper.run("one", () -> template.convertAndSend("q1", "toCluster1")); + wrapper.run("two", () -> template.convertAndSend("q2", "toCluster2")); + wrapper.run("three", () -> template.convertAndSend("q3", "toCluster3")); + }; +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/post-processing.adoc b/src/reference/antora/modules/ROOT/pages/amqp/post-processing.adoc new file mode 100644 index 0000000000..7f01e783cb --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/post-processing.adoc @@ -0,0 +1,35 @@ +[[post-processing]] += Modifying Messages - Compression and More + +A number of extension points exist. +They let you perform some processing on a message, either before it is sent to RabbitMQ or immediately after it is received. + +As can be seen in xref:amqp/message-converters.adoc[Message Converters], one such extension point is in the `AmqpTemplate` `convertAndReceive` operations, where you can provide a `MessagePostProcessor`. +For example, after your POJO has been converted, the `MessagePostProcessor` lets you set custom headers or properties on the `Message`. + +Starting with version 1.4.2, additional extension points have been added to the `RabbitTemplate` - `setBeforePublishPostProcessors()` and `setAfterReceivePostProcessors()`. +The first enables a post processor to run immediately before sending to RabbitMQ. +When using batching (see xref:amqp/sending-messages.adoc#template-batching[Batching]), this is invoked after the batch is assembled and before the batch is sent. +The second is invoked immediately after a message is received. + +These extension points are used for such features as compression and, for this purpose, several `MessagePostProcessor` implementations are provided. +`GZipPostProcessor`, `ZipPostProcessor` and `DeflaterPostProcessor` compress messages before sending, and `GUnzipPostProcessor`, `UnzipPostProcessor` and `InflaterPostProcessor` decompress received messages. + +NOTE: Starting with version 2.1.5, the `GZipPostProcessor` can be configured with the `copyProperties = true` option to make a copy of the original message properties. +By default, these properties are reused for performance reasons, and modified with compression content encoding and the optional `MessageProperties.SPRING_AUTO_DECOMPRESS` header. +If you retain a reference to the original outbound message, its properties will change as well. +So, if your application retains a copy of an outbound message with these message post processors, consider turning the `copyProperties` option on. + +IMPORTANT: Starting with version 2.2.12, you can configure the delimiter that the compressing post processors use between content encoding elements. +With versions 2.2.11 and before, this was hard-coded as `:`, it is now set to `, ` by default. +The decompressors will work with both delimiters. +However, if you publish messages with 2.3 or later and consume with 2.2.11 or earlier, you MUST set the `encodingDelimiter` property on the compressor(s) to `:`. +When your consumers are upgraded to 2.2.11 or later, you can revert to the default of `, `. + +Similarly, the `SimpleMessageListenerContainer` also has a `setAfterReceivePostProcessors()` method, letting the decompression be performed after messages are received by the container. + +Starting with version 2.1.4, `addBeforePublishPostProcessors()` and `addAfterReceivePostProcessors()` have been added to the `RabbitTemplate` to allow appending new post processors to the list of before publish and after receive post processors respectively. +Also there are methods provided to remove the post processors. +Similarly, `AbstractMessageListenerContainer` also has `addAfterReceivePostProcessors()` and `removeAfterReceivePostProcessor()` methods added. +See the Javadoc of `RabbitTemplate` and `AbstractMessageListenerContainer` for more detail. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages.adoc new file mode 100644 index 0000000000..271e10a67f --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages.adoc @@ -0,0 +1,10 @@ +[[receiving-messages]] += Receiving Messages +:page-section-summary-toc: 1 + +Message reception is always a little more complicated than sending. +There are two ways to receive a `Message`. +The simpler option is to poll for one `Message` at a time with a polling method call. +The more complicated yet more common approach is to register a listener that receives `Messages` on-demand, asynchronously. +We consider an example of each approach in the next two sub-sections. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven.adoc new file mode 100644 index 0000000000..35c6691d2a --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven.adoc @@ -0,0 +1,124 @@ +[[async-annotation-driven]] += Annotation-driven Listener Endpoints + +The easiest way to receive a message asynchronously is to use the annotated listener endpoint infrastructure. +In a nutshell, it lets you expose a method of a managed bean as a Rabbit listener endpoint. +The following example shows how to use the `@RabbitListener` annotation: + +[source,java] +---- + +@Component +public class MyService { + + @RabbitListener(queues = "myQueue") + public void processOrder(String data) { + ... + } + +} +---- + +The idea of the preceding example is that, whenever a message is available on the queue named `myQueue`, the `processOrder` method is invoked accordingly (in this case, with the payload of the message). + +The annotated endpoint infrastructure creates a message listener container behind the scenes for each annotated method, by using a `RabbitListenerContainerFactory`. + +In the preceding example, `myQueue` must already exist and be bound to some exchange. +The queue can be declared and bound automatically, as long as a `RabbitAdmin` exists in the application context. + +NOTE: Property placeholders (`${some.property}`) or SpEL expressions (`+#{someExpression}+`) can be specified for the annotation properties (`queues` etc). +See xref:amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc[Listening to Multiple Queues] for an example of why you might use SpEL instead of a property placeholder. +The following listing shows three examples of how to declare a Rabbit listener: + +[source,java] +---- + +@Component +public class MyService { + + @RabbitListener(bindings = @QueueBinding( + value = @Queue(value = "myQueue", durable = "true"), + exchange = @Exchange(value = "auto.exch", ignoreDeclarationExceptions = "true"), + key = "orderRoutingKey") + ) + public void processOrder(Order order) { + ... + } + + @RabbitListener(bindings = @QueueBinding( + value = @Queue, + exchange = @Exchange(value = "auto.exch"), + key = "invoiceRoutingKey") + ) + public void processInvoice(Invoice invoice) { + ... + } + + @RabbitListener(queuesToDeclare = @Queue(name = "${my.queue}", durable = "true")) + public String handleWithSimpleDeclare(String data) { + ... + } + +} +---- + +In the first example, a queue `myQueue` is declared automatically (durable) together with the exchange, if needed, +and bound to the exchange with the routing key. +In the second example, an anonymous (exclusive, auto-delete) queue is declared and bound; the queue name is created by the framework using the `Base64UrlNamingStrategy`. +You cannot declare broker-named queues using this technique; they need to be declared as bean definitions; see xref:amqp/containers-and-broker-named-queues.adoc[Containers and Broker-Named queues]. +Multiple `QueueBinding` entries can be provided, letting the listener listen to multiple queues. +In the third example, a queue with the name retrieved from property `my.queue` is declared, if necessary, with the default binding to the default exchange using the queue name as the routing key. + +Since version 2.0, the `@Exchange` annotation supports any exchange types, including custom. +For more information, see https://www.rabbitmq.com/tutorials/amqp-concepts.html[AMQP Concepts]. + +You can use normal `@Bean` definitions when you need more advanced configuration. + +Notice `ignoreDeclarationExceptions` on the exchange in the first example. +This allows, for example, binding to an existing exchange that might have different settings (such as `internal`). +By default, the properties of an existing exchange must match. + +Starting with version 2.0, you can now bind a queue to an exchange with multiple routing keys, as the following example shows: + +[source, java] +---- +... + key = { "red", "yellow" } +... +---- + +You can also specify arguments within `@QueueBinding` annotations for queues, exchanges, +and bindings, as the following example shows: + +[source, java] +---- +@RabbitListener(bindings = @QueueBinding( + value = @Queue(value = "auto.headers", autoDelete = "true", + arguments = @Argument(name = "x-message-ttl", value = "10000", + type = "java.lang.Integer")), + exchange = @Exchange(value = "auto.headers", type = ExchangeTypes.HEADERS, autoDelete = "true"), + arguments = { + @Argument(name = "x-match", value = "all"), + @Argument(name = "thing1", value = "somevalue"), + @Argument(name = "thing2") + }) +) +public String handleWithHeadersExchange(String foo) { + ... +} +---- + +Notice that the `x-message-ttl` argument is set to 10 seconds for the queue. +Since the argument type is not `String`, we have to specify its type -- in this case, `Integer`. +As with all such declarations, if the queue already exists, the arguments must match those on the queue. +For the header exchange, we set the binding arguments to match messages that have the `thing1` header set to `somevalue`, and +the `thing2` header must be present with any value. +The `x-match` argument means both conditions must be satisfied. + +The argument name, value, and type can be property placeholders (`${...}`) or SpEL expressions (`#{...}`). +The `name` must resolve to a `String`. +The expression for `type` must resolve to a `Class` or the fully-qualified name of a class. +The `value` must resolve to something that can be converted by the `DefaultConversionService` to the type (such as the `x-message-ttl` in the preceding example). + +If a name resolves to `null` or an empty `String`, that `@Argument` is ignored. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc new file mode 100644 index 0000000000..b9d71cc174 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc @@ -0,0 +1,23 @@ +[[container-management]] += Container Management +:page-section-summary-toc: 1 + +Containers created for annotations are not registered with the application context. +You can obtain a collection of all containers by invoking `getListenerContainers()` on the +`RabbitListenerEndpointRegistry` bean. +You can then iterate over this collection, for example, to stop or start all containers or invoke the `Lifecycle` methods +on the registry itself, which will invoke the operations on each container. + +You can also get a reference to an individual container by using its `id`, using `getListenerContainer(String id)` -- for +example, `registry.getListenerContainer("multi")` for the container created by the snippet above. + +Starting with version 1.5.2, you can obtain the `id` values of the registered containers with `getListenerContainerIds()`. + +Starting with version 1.5, you can now assign a `group` to the container on the `RabbitListener` endpoint. +This provides a mechanism to get a reference to a subset of containers. +Adding a `group` attribute causes a bean of type `Collection` to be registered with the context with the group name. + +By default, stopping a container will cancel the consumer and process all prefetched messages before stopping. +Starting with versions 2.4.14, 3.0.6, you can set the xref:amqp/containerAttributes.adoc#forceStop[`forceStop`] container property to true to stop immediately after the current message is processed, causing any prefetched messages to be requeued. +This is useful, for example, if exclusive or single-active consumers are being used. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc new file mode 100644 index 0000000000..cd18a60185 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc @@ -0,0 +1,101 @@ +[[async-annotation-conversion]] += Message Conversion for Annotated Methods + +There are two conversion steps in the pipeline before invoking the listener. +The first step uses a `MessageConverter` to convert the incoming Spring AMQP `Message` to a Spring-messaging `Message`. +When the target method is invoked, the message payload is converted, if necessary, to the method parameter type. + +The default `MessageConverter` for the first step is a Spring AMQP `SimpleMessageConverter` that handles conversion to +`String` and `java.io.Serializable` objects. +All others remain as a `byte[]`. +In the following discussion, we call this the "`message converter`". + +The default converter for the second step is a `GenericMessageConverter`, which delegates to a conversion service +(an instance of `DefaultFormattingConversionService`). +In the following discussion, we call this the "`method argument converter`". + +To change the message converter, you can add it as a property to the container factory bean. +The following example shows how to do so: + +[source, java] +---- +@Bean +public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + ... + factory.setMessageConverter(new Jackson2JsonMessageConverter()); + ... + return factory; +} +---- + +This configures a Jackson2 converter that expects header information to be present to guide the conversion. + +You can also use a `ContentTypeDelegatingMessageConverter`, which can handle conversion of different content types. + +Starting with version 2.3, you can override the factory converter by specifying a bean name in the `messageConverter` property. + +[source, java] +---- +@Bean +public Jackson2JsonMessageConverter jsonConverter() { + return new Jackson2JsonMessageConverter(); +} + +@RabbitListener(..., messageConverter = "jsonConverter") +public void listen(String in) { + ... +} +---- + +This avoids having to declare a different container factory just to change the converter. + +In most cases, it is not necessary to customize the method argument converter unless, for example, you want to use +a custom `ConversionService`. + +In versions prior to 1.6, the type information to convert the JSON had to be provided in message headers, or a +custom `ClassMapper` was required. +Starting with version 1.6, if there are no type information headers, the type can be inferred from the target +method arguments. + +NOTE: This type inference works only for `@RabbitListener` at the method level. + +See xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. + +If you wish to customize the method argument converter, you can do so as follows: + +[source, java] +---- +@Configuration +@EnableRabbit +public class AppConfig implements RabbitListenerConfigurer { + + ... + + @Bean + public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() { + DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory(); + factory.setMessageConverter(new GenericMessageConverter(myConversionService())); + return factory; + } + + @Bean + public DefaultConversionService myConversionService() { + DefaultConversionService conv = new DefaultConversionService(); + conv.addConverter(mySpecialConverter()); + return conv; + } + + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory()); + } + + ... + +} +---- + +IMPORTANT: For multi-method listeners (see xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[Multi-method Listeners]), the method selection is based on the payload of the message *after the message conversion*. +The method argument converter is called only after the method has been selected. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/custom-argument-resolver.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/custom-argument-resolver.adoc new file mode 100644 index 0000000000..8ee48932f3 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/custom-argument-resolver.adoc @@ -0,0 +1,36 @@ +[[custom-argument-resolver]] += Adding a Custom `HandlerMethodArgumentResolver` to @RabbitListener + +Starting with version 2.3.7 you are able to add your own `HandlerMethodArgumentResolver` and resolve custom method parameters. +All you need is to implement `RabbitListenerConfigurer` and use method `setCustomMethodArgumentResolvers()` from class `RabbitListenerEndpointRegistrar`. + +[source, java] +---- +@Configuration +class CustomRabbitConfig implements RabbitListenerConfigurer { + + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setCustomMethodArgumentResolvers( + new HandlerMethodArgumentResolver() { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return CustomMethodArgument.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, org.springframework.messaging.Message message) { + return new CustomMethodArgument( + (String) message.getPayload(), + message.getHeaders().get("customHeader", String.class) + ); + } + + } + ); + } + +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable-signature.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable-signature.adoc new file mode 100644 index 0000000000..ed5464b829 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable-signature.adoc @@ -0,0 +1,71 @@ +[[async-annotation-driven-enable-signature]] += Annotated Endpoint Method Signature + +So far, we have been injecting a simple `String` in our endpoint, but it can actually have a very flexible method signature. +The following example rewrites it to inject the `Order` with a custom header: + +[source,java] +---- +@Component +public class MyService { + + @RabbitListener(queues = "myQueue") + public void processOrder(Order order, @Header("order_type") String orderType) { + ... + } +} +---- + +The following list shows the arguments that are available to be matched with parameters in listener endpoints: + +* The raw `org.springframework.amqp.core.Message`. +* The `MessageProperties` from the raw `Message`. +* The `com.rabbitmq.client.Channel` on which the message was received. +* The `org.springframework.messaging.Message` converted from the incoming AMQP message. +* `@Header`-annotated method arguments to extract a specific header value, including standard AMQP headers. +* `@Headers`-annotated argument that must also be assignable to `java.util.Map` for getting access to all headers. +* The converted payload + +A non-annotated element that is not one of the supported types (that is, +`Message`, `MessageProperties`, `Message` and `Channel`) is matched with the payload. +You can make that explicit by annotating the parameter with `@Payload`. +You can also turn on validation by adding an extra `@Valid`. + +The ability to inject Spring’s message abstraction is particularly useful to benefit from all the information stored in the transport-specific message without relying on the transport-specific API. +The following example shows how to do so: + +[source,java] +---- + +@RabbitListener(queues = "myQueue") +public void processOrder(Message order) { ... +} + +---- + +Handling of method arguments is provided by `DefaultMessageHandlerMethodFactory`, which you can further customize to support additional method arguments. +The conversion and validation support can be customized there as well. + +For instance, if we want to make sure our `Order` is valid before processing it, we can annotate the payload with `@Valid` and configure the necessary validator, as follows: + +[source,java] +---- + +@Configuration +@EnableRabbit +public class AppConfig implements RabbitListenerConfigurer { + + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory()); + } + + @Bean + public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() { + DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory(); + factory.setValidator(myValidator()); + return factory; + } +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc new file mode 100644 index 0000000000..c9d37e7a09 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc @@ -0,0 +1,121 @@ +[[async-annotation-driven-enable]] += Enable Listener Endpoint Annotations + +To enable support for `@RabbitListener` annotations, you can add `@EnableRabbit` to one of your `@Configuration` classes. +The following example shows how to do so: + +[source,java] +---- +@Configuration +@EnableRabbit +public class AppConfig { + + @Bean + public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + factory.setConcurrentConsumers(3); + factory.setMaxConcurrentConsumers(10); + factory.setContainerCustomizer(container -> /* customize the container */); + return factory; + } +} +---- + +Since version 2.0, a `DirectMessageListenerContainerFactory` is also available. +It creates `DirectMessageListenerContainer` instances. + +NOTE: For information to help you choose between `SimpleRabbitListenerContainerFactory` and `DirectRabbitListenerContainerFactory`, see xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container]. + +Starting with version 2.2.2, you can provide a `ContainerCustomizer` implementation (as shown above). +This can be used to further configure the container after it has been created and configured; you can use this, for example, to set properties that are not exposed by the container factory. + +Version 2.4.8 provides the `CompositeContainerCustomizer` for situations where you wish to apply multiple customizers. + +By default, the infrastructure looks for a bean named `rabbitListenerContainerFactory` as the source for the factory to use to create message listener containers. +In this case, and ignoring the RabbitMQ infrastructure setup, the `processOrder` method can be invoked with a core poll 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 `RabbitListenerConfigurer` interface. +The default is required only if at least one endpoint is registered without a specific container factory. +See the javadoc:org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer[Javadoc] for full details and examples. + +The container factories provide methods for adding `MessagePostProcessor` instances that are applied after receiving messages (before invoking the listener) and before sending replies. + +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for information about replies. + +Starting with version 2.0.6, you can add a `RetryTemplate` and `RecoveryCallback` to the listener container factory. +It is used when sending replies. +The `RecoveryCallback` is invoked when retries are exhausted. +You can use a `SendRetryContextAccessor` to get information from the context. +The following example shows how to do so: + +[source, java] +---- +factory.setRetryTemplate(retryTemplate); +factory.setReplyRecoveryCallback(ctx -> { + Message failed = SendRetryContextAccessor.getMessage(ctx); + Address replyTo = SendRetryContextAccessor.getAddress(ctx); + Throwable t = ctx.getLastThrowable(); + ... + return null; +}); +---- + +If you prefer XML configuration, you can use the `` element. +Any beans annotated with `@RabbitListener` are detected. + +For `SimpleRabbitListenerContainer` instances, you can use XML similar to the following: + +[source,xml] +---- + + + + + + + +---- + +For `DirectMessageListenerContainer` instances, you can use XML similar to the following: + +[source,xml] +---- + + + + + + +---- + + +[[listener-property-overrides]] +Starting with version 2.0, the `@RabbitListener` annotation has a `concurrency` property. +It supports SpEL expressions (`#{...}`) and property placeholders (`${...}`). +Its meaning and allowed values depend on the container type, as follows: + +* For the `DirectMessageListenerContainer`, the value must be a single integer value, which sets the `consumersPerQueue` property on the container. +* For the `SimpleRabbitListenerContainer`, the value can be a single integer value, which sets the `concurrentConsumers` property on the container, or it can have the form, `m-n`, where `m` is the `concurrentConsumers` property and `n` is the `maxConcurrentConsumers` property. + +In either case, this setting overrides the settings on the factory. +Previously you had to define different container factories if you had listeners that required different concurrency. + +The annotation also allows overriding the factory `autoStartup` and `taskExecutor` properties via the `autoStartup` and `executor` (since 2.2) annotation properties. +Using a different executor for each might help with identifying threads associated with each listener in logs and thread dumps. + +Version 2.2 also added the `ackMode` property, which allows you to override the container factory's `acknowledgeMode` property. + +[source, java] +---- +@RabbitListener(id = "manual.acks.1", queues = "manual.acks.1", ackMode = "MANUAL") +public void manual1(String in, Channel channel, + @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException { + + ... + channel.basicAck(tag, false); +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc new file mode 100644 index 0000000000..fe273d6adf --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc @@ -0,0 +1,55 @@ +[[annotation-error-handling]] += Handling Exceptions + +By default, if an annotated listener method throws an exception, it is thrown to the container and the message are requeued and redelivered, discarded, or routed to a dead letter exchange, depending on the container and broker configuration. +Nothing is returned to the sender. + +Starting with version 2.0, the `@RabbitListener` annotation has two new attributes: `errorHandler` and `returnExceptions`. + +These are not configured by default. + +You can use the `errorHandler` to provide the bean name of a `RabbitListenerErrorHandler` implementation. +This functional interface has one method, as follows: + +[source, java] +---- +@FunctionalInterface +public interface RabbitListenerErrorHandler { + + Object handleError(Message amqpMessage, org.springframework.messaging.Message message, + ListenerExecutionFailedException exception) throws Exception; + +} +---- + +As you can see, you have access to the raw message received from the container, the spring-messaging `Message` object produced by the message converter, and the exception that was thrown by the listener (wrapped in a `ListenerExecutionFailedException`). +The error handler can either return some result (which is sent as the reply) or throw the original or a new exception (which is thrown to the container or returned to the sender, depending on the `returnExceptions` setting). + +The `returnExceptions` attribute, when `true`, causes exceptions to be returned to the sender. +The exception is wrapped in a `RemoteInvocationResult` object. +On the sender side, there is an available `RemoteInvocationAwareMessageConverterAdapter`, which, if configured into the `RabbitTemplate`, re-throws the server-side exception, wrapped in an `AmqpRemoteException`. +The stack trace of the server exception is synthesized by merging the server and client stack traces. + +IMPORTANT: This mechanism generally works only with the default `SimpleMessageConverter`, which uses Java serialization. +Exceptions are generally not "`Jackson-friendly`" and cannot be serialized to JSON. +If you use JSON, consider using an `errorHandler` to return some other Jackson-friendly `Error` object when an exception is thrown. + +IMPORTANT: In version 2.1, this interface moved from package `o.s.amqp.rabbit.listener` to `o.s.amqp.rabbit.listener.api`. + +Starting with version 2.1.7, the `Channel` is available in a messaging message header; this allows you to ack or nack the failed message when using `AcknowledgeMode.MANUAL`: + +[source, java] +---- +public Object handleError(Message amqpMessage, org.springframework.messaging.Message message, + ListenerExecutionFailedException exception) { + ... + message.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class) + .basicReject(message.getHeaders().get(AmqpHeaders.DELIVERY_TAG, Long.class), + true); + } +---- + +Starting with version 2.2.18, if a message conversion exception is thrown, the error handler will be called, with `null` in the `message` argument. +This allows the application to send some result to the caller, indicating that a badly-formed message was received. +Previously, such errors were thrown and handled by the container. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/meta.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/meta.adoc new file mode 100644 index 0000000000..9ef2318ba3 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/meta.adoc @@ -0,0 +1,71 @@ +[[meta-annotation-driven]] += Meta-annotations + +Sometimes you may want to use the same configuration for multiple listeners. +To reduce the boilerplate configuration, you can use meta-annotations to create your own listener annotation. +The following example shows how to do so: + +[source, java] +---- +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@RabbitListener(bindings = @QueueBinding( + value = @Queue, + exchange = @Exchange(value = "metaFanout", type = ExchangeTypes.FANOUT))) +public @interface MyAnonFanoutListener { +} + +public class MetaListener { + + @MyAnonFanoutListener + public void handle1(String foo) { + ... + } + + @MyAnonFanoutListener + public void handle2(String foo) { + ... + } + +} +---- + +In the preceding example, each listener created by the `@MyAnonFanoutListener` annotation binds an anonymous, auto-delete +queue to the fanout exchange, `metaFanout`. +Starting with version 2.2.3, `@AliasFor` is supported to allow overriding properties on the meta-annotated annotation. +Also, user annotations can now be `@Repeatable`, allowing multiple containers to be created for a method. + +[source, java] +---- +@Component +static class MetaAnnotationTestBean { + + @MyListener("queue1") + @MyListener("queue2") + public void handleIt(String body) { + } + +} + + +@RabbitListener +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(MyListeners.class) +static @interface MyListener { + + @AliasFor(annotation = RabbitListener.class, attribute = "queues") + String[] value() default {}; + +} + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +static @interface MyListeners { + + MyListener[] value(); + +} +---- + + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/method-selection.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/method-selection.adoc new file mode 100644 index 0000000000..0fba2986f2 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/method-selection.adoc @@ -0,0 +1,47 @@ +[[annotation-method-selection]] += Multi-method Listeners + +Starting with version 1.5.0, you can specify the `@RabbitListener` annotation at the class level. +Together with the new `@RabbitHandler` annotation, this lets a single listener invoke different methods, based on +the payload type of the incoming message. +This is best described using an example: + +[source, java] +---- +@RabbitListener(id="multi", queues = "someQueue") +@SendTo("my.reply.queue") +public class MultiListenerBean { + + @RabbitHandler + public String thing2(Thing2 thing2) { + ... + } + + @RabbitHandler + public String cat(Cat cat) { + ... + } + + @RabbitHandler + public String hat(@Header("amqp_receivedRoutingKey") String rk, @Payload Hat hat) { + ... + } + + @RabbitHandler(isDefault = true) + public String defaultMethod(Object object) { + ... + } + +} +---- + +In this case, the individual `@RabbitHandler` methods are invoked if the converted payload is a `Thing2`, a `Cat`, or a `Hat`. +You should understand that the system must be able to identify a unique method based on the payload type. +The type is checked for assignability to a single parameter that has no annotations or that is annotated with the `@Payload` annotation. +Notice that the same method signatures apply, as discussed in the method-level `@RabbitListener` (xref:amqp/receiving-messages/async-consumer.adoc#message-listener-adapter[described earlier]). + +Starting with version 2.0.3, a `@RabbitHandler` method can be designated as the default method, which is invoked if there is no match on other methods. +At most, one method can be so designated. + +IMPORTANT: `@RabbitHandler` is intended only for processing message payloads after conversion, if you wish to receive the unconverted raw `Message` object, you must use `@RabbitListener` on the method, not the class. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc new file mode 100644 index 0000000000..8ac6879654 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc @@ -0,0 +1,40 @@ +[[annotation-multiple-queues]] += Listening to Multiple Queues + +When you use the `queues` attribute, you can specify that the associated container can listen to multiple queues. +You can use a `@Header` annotation to make the queue name from which a message was received available to the POJO +method. +The following example shows how to do so: + +[source, java] +---- +@Component +public class MyService { + + @RabbitListener(queues = { "queue1", "queue2" } ) + public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { + ... + } + +} +---- + +Starting with version 1.5, you can externalize the queue names by using property placeholders and SpEL. +The following example shows how to do so: + +[source, java] +---- +@Component +public class MyService { + + @RabbitListener(queues = "#{'${property.with.comma.delimited.queue.names}'.split(',')}" ) + public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { + ... + } + +} +---- + +Prior to version 1.5, only a single queue could be specified this way. +Each queue needed a separate property. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/proxy-rabbitlistener-and-generics.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/proxy-rabbitlistener-and-generics.adoc new file mode 100644 index 0000000000..0f625de9a8 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/proxy-rabbitlistener-and-generics.adoc @@ -0,0 +1,46 @@ +[[proxy-rabbitlistener-and-generics]] += Proxy `@RabbitListener` and Generics + +If your service is intended to be proxied (for example, in the case of `@Transactional`), you should keep in mind some considerations when +the interface has generic parameters. +Consider the following example: + +[source, java] +---- +interface TxService

{ + + String handle(P payload, String header); + +} + +static class TxServiceImpl implements TxService { + + @Override + @RabbitListener(...) + public String handle(Thing thing, String rk) { + ... + } + +} +---- + +With a generic interface and a particular implementation, you are forced to switch to the CGLIB target class proxy because the actual implementation of the interface +`handle` method is a bridge method. +In the case of transaction management, the use of CGLIB is configured by using +an annotation option: `@EnableTransactionManagement(proxyTargetClass = true)`. +And in this case, all annotations have to be declared on the target method in the implementation, as the following example shows: + +[source, java] +---- +static class TxServiceImpl implements TxService { + + @Override + @Transactional + @RabbitListener(...) + public String handle(@Payload Foo foo, @Header("amqp_receivedRoutingKey") String rk) { + ... + } + +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/rabbit-validation.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/rabbit-validation.adoc new file mode 100644 index 0000000000..784c8c8305 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/rabbit-validation.adoc @@ -0,0 +1,69 @@ +[[rabbit-validation]] += @RabbitListener @Payload Validation + +Starting with version 2.3.7, it is now easier to add a `Validator` to validate `@RabbitListener` and `@RabbitHandler` `@Payload` arguments. +Now, you can simply add the validator to the registrar itself. + +[source, java] +---- +@Configuration +@EnableRabbit +public class Config implements RabbitListenerConfigurer { + ... + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setValidator(new MyValidator()); + } +} +---- + +NOTE: When using Spring Boot with the validation starter, a `LocalValidatorFactoryBean` is auto-configured: + +[source, java] +---- +@Configuration +@EnableRabbit +public class Config implements RabbitListenerConfigurer { + @Autowired + private LocalValidatorFactoryBean validator; + ... + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setValidator(this.validator); + } +} +---- + +To validate: + +[source, java] +---- +public static class ValidatedClass { + @Max(10) + private int bar; + public int getBar() { + return this.bar; + } + public void setBar(int bar) { + this.bar = bar; + } +} +---- + +and + +[source, java] +---- +@RabbitListener(id="validated", queues = "queue1", errorHandler = "validationErrorHandler", + containerFactory = "jsonListenerContainerFactory") +public void validatedListener(@Payload @Valid ValidatedClass val) { + ... +} +@Bean +public RabbitListenerErrorHandler validationErrorHandler() { + return (m, e) -> { + ... + }; +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc new file mode 100644 index 0000000000..46db7e95b5 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc @@ -0,0 +1,32 @@ +[[async-annotation-driven-registration]] += Programmatic Endpoint Registration + +`RabbitListenerEndpoint` provides a model of a Rabbit endpoint and is responsible for configuring the container for that model. +The infrastructure lets you configure endpoints programmatically in addition to the ones that are detected by the `RabbitListener` annotation. +The following example shows how to do so: + +[source,java] +---- +@Configuration +@EnableRabbit +public class AppConfig implements RabbitListenerConfigurer { + + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); + endpoint.setId("someRabbitListenerEndpoint"); + endpoint.setQueueNames("anotherQueue"); + endpoint.setMessageListener(message -> { + // processing + }); + registrar.registerEndpoint(endpoint); + } +} +---- + +In the preceding example, we used `SimpleRabbitListenerEndpoint`, which provides the actual `MessageListener` to invoke, but you could just as well build your own endpoint variant to describe a custom invocation mechanism. + +NOTE: the `id` property is required for `SimpleRabbitListenerEndpoint` definition. + +It should be noted that you could just as well skip the use of `@RabbitListener` altogether and register your endpoints programmatically through `RabbitListenerConfigurer`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc new file mode 100644 index 0000000000..cac86f7174 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc @@ -0,0 +1,10 @@ +[[repeatable-rabbit-listener]] += `@Repeatable` `@RabbitListener` +:page-section-summary-toc: 1 + +Starting with version 1.6, the `@RabbitListener` annotation is marked with `@Repeatable`. +This means that the annotation can appear on the same annotated element (method or class) multiple times. +In this case, a separate listener container is created for each annotation, each of which invokes the same listener +`@Bean`. +Repeatable annotations can be used with Java 8 or above. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc new file mode 100644 index 0000000000..b8fc03bd19 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc @@ -0,0 +1,51 @@ +[[reply-content-type]] += Reply ContentType + +If you are using a sophisticated message converter, such as the `ContentTypeDelegatingMessageConverter`, you can control the content type of the reply by setting the `replyContentType` property on the listener. +This allows the converter to select the appropriate delegate converter for the reply. + +[source, java] +---- +@RabbitListener(queues = "q1", messageConverter = "delegating", + replyContentType = "application/json") +public Thing2 listen(Thing1 in) { + ... +} +---- + +By default, for backwards compatibility, any content type property set by the converter will be overwritten by this value after conversion. +Converters such as the `SimpleMessageConverter` use the reply type rather than the content type to determine the conversion needed and sets the content type in the reply message appropriately. +This may not be the desired action and can be overridden by setting the `converterWinsContentType` property to `false`. +For example, if you return a `String` containing JSON, the `SimpleMessageConverter` will set the content type in the reply to `text/plain`. +The following configuration will ensure the content type is set properly, even if the `SimpleMessageConverter` is used. + +[source, java] +---- +@RabbitListener(queues = "q1", replyContentType = "application/json", + converterWinsContentType = "false") +public String listen(Thing in) { + ... + return someJsonString; +} +---- + +These properties (`replyContentType` and `converterWinsContentType`) do not apply when the return type is a Spring AMQP `Message` or a Spring Messaging `Message`. +In the first case, there is no conversion involved; simply set the `contentType` message property. +In the second case, the behavior is controlled using message headers: + +[source, java] +---- +@RabbitListener(queues = "q1", messageConverter = "delegating") +@SendTo("q2") +public Message listen(String in) { + ... + return MessageBuilder.withPayload(in.toUpperCase()) + .setHeader(MessageHeaders.CONTENT_TYPE, "application/xml") + .build(); +} +---- + +This content type will be passed in the `MessageProperties` to the converter. +By default, for backwards compatibility, any content type property set by the converter will be overwritten by this value after conversion. +If you wish to override that behavior, also set the `AmqpHeaders.CONTENT_TYPE_CONVERTER_WINS` to `true` and any value set by the converter will be retained. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply.adoc new file mode 100644 index 0000000000..6c9c584614 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply.adoc @@ -0,0 +1,158 @@ +[[async-annotation-driven-reply]] += Reply Management + +The existing support in `MessageListenerAdapter` already lets your method have a non-void return type. +When that is the case, the result of the invocation is encapsulated in a message sent to the address specified in the `ReplyToAddress` header of the original message, or to the default address configured on the listener. +You can set that default address by using the `@SendTo` annotation of the messaging abstraction. + +Assuming our `processOrder` method should now return an `OrderStatus`, we can write it as follows to automatically send a reply: + +[source,java] +---- +@RabbitListener(destination = "myQueue") +@SendTo("status") +public OrderStatus processOrder(Order order) { + // order processing + return status; +} +---- + +If you need to set additional headers in a transport-independent manner, you could return a `Message` instead, something like the following: + +[source,java] +---- + +@RabbitListener(destination = "myQueue") +@SendTo("status") +public Message processOrder(Order order) { + // order processing + return MessageBuilder + .withPayload(status) + .setHeader("code", 1234) + .build(); +} +---- + +Alternatively, you can use a `MessagePostProcessor` in the `beforeSendReplyMessagePostProcessors` container factory property to add more headers. +Starting with version 2.2.3, the called bean/method is made available in the reply message, which can be used in a message post processor to communicate the information back to the caller: + +[source, java] +---- +factory.setBeforeSendReplyPostProcessors(msg -> { + msg.getMessageProperties().setHeader("calledBean", + msg.getMessageProperties().getTargetBean().getClass().getSimpleName()); + msg.getMessageProperties().setHeader("calledMethod", + msg.getMessageProperties().getTargetMethod().getName()); + return m; +}); +---- + +Starting with version 2.2.5, you can configure a `ReplyPostProcessor` to modify the reply message before it is sent; it is called after the `correlationId` header has been set up to match the request. + +[source, java] +---- +@RabbitListener(queues = "test.header", group = "testGroup", replyPostProcessor = "echoCustomHeader") +public String capitalizeWithHeader(String in) { + return in.toUpperCase(); +} + +@Bean +public ReplyPostProcessor echoCustomHeader() { + return (req, resp) -> { + resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader")); + return resp; + }; +} +---- + +Starting with version 3.0, you can configure the post processor on the container factory instead of on the annotation. + +[source, java] +---- +factory.setReplyPostProcessorProvider(id -> (req, resp) -> { + resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader")); + return resp; +}); +---- + +The `id` parameter is the listener id. + +A setting on the annotation will supersede the factory setting. + +The `@SendTo` value is assumed as a reply `exchange` and `routingKey` pair that follows the `exchange/routingKey` pattern, +where one of those parts can be omitted. +The valid values are as follows: + +* `thing1/thing2`: The `replyTo` exchange and the `routingKey`. +`thing1/`: The `replyTo` exchange and the default (empty) `routingKey`. +`thing2` or `/thing2`: The `replyTo` `routingKey` and the default (empty) exchange. +`/` or empty: The `replyTo` default exchange and the default `routingKey`. + +Also, you can use `@SendTo` without a `value` attribute. +This case is equal to an empty `sendTo` pattern. +`@SendTo` is used only if the inbound message does not have a `replyToAddress` property. + +Starting with version 1.5, the `@SendTo` value can be a bean initialization SpEL Expression, as shown in the following example: + +[source, java] +---- +@RabbitListener(queues = "test.sendTo.spel") +@SendTo("#{spelReplyTo}") +public String capitalizeWithSendToSpel(String foo) { + return foo.toUpperCase(); +} +... +@Bean +public String spelReplyTo() { + return "test.sendTo.reply.spel"; +} +---- + +The expression must evaluate to a `String`, which can be a simple queue name (sent to the default exchange) or with +the form `exchange/routingKey` as discussed prior to the preceding example. + +NOTE: The `#{...}` expression is evaluated once, during initialization. + +For dynamic reply routing, the message sender should include a `reply_to` message property or use the alternate +runtime SpEL expression (described after the next example). + +Starting with version 1.6, the `@SendTo` can be a SpEL expression that is evaluated at runtime against the request +and reply, as the following example shows: + +[source, java] +---- +@RabbitListener(queues = "test.sendTo.spel") +@SendTo("!{'some.reply.queue.with.' + result.queueName}") +public Bar capitalizeWithSendToSpel(Foo foo) { + return processTheFooAndReturnABar(foo); +} +---- + +The runtime nature of the SpEL expression is indicated with `!{...}` delimiters. +The evaluation context `#root` object for the expression has three properties: + +* `request`: The `o.s.amqp.core.Message` request object. +* `source`: The `o.s.messaging.Message` after conversion. +* `result`: The method result. + +The context has a map property accessor, a standard type converter, and a bean resolver, which lets other beans in the +context be referenced (for example, `@someBeanName.determineReplyQ(request, result)`). + +In summary, `#{...}` is evaluated once during initialization, with the `#root` object being the application context. +Beans are referenced by their names. +`!{...}` is evaluated at runtime for each message, with the root object having the properties listed earlier. +Beans are referenced with their names, prefixed by `@`. + +Starting with version 2.1, simple property placeholders are also supported (for example, `${some.reply.to}`). +With earlier versions, the following can be used as a work around, as the following example shows: + +[source, java] +---- +@RabbitListener(queues = "foo") +@SendTo("#{environment['my.send.to']}") +public String listen(Message in) { + ... + return ... +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc new file mode 100644 index 0000000000..c7b0f33374 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc @@ -0,0 +1,275 @@ +[[async-consumer]] += Asynchronous Consumer + +IMPORTANT: Spring AMQP also supports annotated listener endpoints through the use of the `@RabbitListener` annotation and provides an open infrastructure to register endpoints programmatically. +This is by far the most convenient way to setup an asynchronous consumer. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more details. + +[IMPORTANT] +==== +The prefetch default value used to be 1, which could lead to under-utilization of efficient consumers. +Starting with version 2.0, the default prefetch value is now 250, which should keep consumers busy in most common scenarios and +thus improve throughput. + +There are, nevertheless, scenarios where the prefetch value should be low: + +* For large messages, especially if the processing is slow (messages could add up to a large amount of memory in the client process) +* When strict message ordering is necessary (the prefetch value should be set back to 1 in this case) +* Other special cases + +Also, with low-volume messaging and multiple consumers (including concurrency within a single listener container instance), you may wish to reduce the prefetch to get a more even distribution of messages across consumers. + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + +For more background about prefetch, see this post about https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/[consumer utilization in RabbitMQ] +and this post about https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/[queuing theory]. +==== + +[[message-listener]] +== Message Listener + +For asynchronous `Message` reception, a dedicated component (not the `AmqpTemplate`) is involved. +That component is a container for a `Message`-consuming callback. +We consider the container and its properties later in this section. +First, though, we should look at the callback, since that is where your application code is integrated with the messaging system. +There are a few options for the callback, starting with an implementation of the `MessageListener` interface, which the following listing shows: + +[source,java] +---- +public interface MessageListener { + void onMessage(Message message); +} +---- + +If your callback logic depends on the AMQP Channel instance for any reason, you may instead use the `ChannelAwareMessageListener`. +It looks similar but has an extra parameter. +The following listing shows the `ChannelAwareMessageListener` interface definition: + +[source,java] +---- +public interface ChannelAwareMessageListener { + void onMessage(Message message, Channel channel) throws Exception; +} +---- + +IMPORTANT: In version 2.1, this interface moved from package `o.s.amqp.rabbit.core` to `o.s.amqp.rabbit.listener.api`. + +[[message-listener-adapter]] +== `MessageListenerAdapter` + +If you prefer to maintain a stricter separation between your application logic and the messaging API, you can rely upon an adapter implementation that is provided by the framework. +This is often referred to as "`Message-driven POJO`" support. + +NOTE: Version 1.5 introduced a more flexible mechanism for POJO messaging, the `@RabbitListener` annotation. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +When using the adapter, you need to provide only a reference to the instance that the adapter itself should invoke. +The following example shows how to do so: + +[source,java] +---- +MessageListenerAdapter listener = new MessageListenerAdapter(somePojo); +listener.setDefaultListenerMethod("myMethod"); +---- + +You can subclass the adapter and provide an implementation of `getListenerMethodName()` to dynamically select different methods based on the message. +This method has two parameters, `originalMessage` and `extractedMessage`, the latter being the result of any conversion. +By default, a `SimpleMessageConverter` is configured. +See xref:amqp/message-converters.adoc#simple-message-converter[`SimpleMessageConverter`] for more information and information about other converters available. + +Starting with version 1.4.2, the original message has `consumerQueue` and `consumerTag` properties, which can be used to determine the queue from which a message was received. + +Starting with version 1.5, you can configure a map of consumer queue or tag to method name, to dynamically select the method to call. +If no entry is in the map, we fall back to the default listener method. +The default listener method (if not set) is `handleMessage`. + +Starting with version 2.0, a convenient `FunctionalInterface` has been provided. +The following listing shows the definition of `FunctionalInterface`: + +[source, java] +---- +@FunctionalInterface +public interface ReplyingMessageListener { + + R handleMessage(T t); + +} +---- + +This interface facilitates convenient configuration of the adapter by using Java 8 lambdas, as the following example shows: + +[source, java] +---- +new MessageListenerAdapter((ReplyingMessageListener) data -> { + ... + return result; +})); +---- + +Starting with version 2.2, the `buildListenerArguments(Object)` has been deprecated and new `buildListenerArguments(Object, Channel, Message)` one has been introduced instead. +The new method helps listener to get `Channel` and `Message` arguments to do more, such as calling `channel.basicReject(long, boolean)` in manual acknowledge mode. +The following listing shows the most basic example: + +[source,java] +---- +public class ExtendedListenerAdapter extends MessageListenerAdapter { + + @Override + protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { + return new Object[]{extractedMessage, channel, message}; + } + +} +---- + +Now you could configure `ExtendedListenerAdapter` as same as `MessageListenerAdapter` if you need to receive "`channel`" and "`message`". +Parameters of listener should be set as `buildListenerArguments(Object, Channel, Message)` returned, as the following example of listener shows: + +[source,java] +---- +public void handleMessage(Object object, Channel channel, Message message) throws IOException { + ... +} +---- + +[[container]] +== Container + +Now that you have seen the various options for the `Message`-listening callback, we can turn our attention to the container. +Basically, the container handles the "`active`" responsibilities so that the listener callback can remain passive. +The container is an example of a "`lifecycle`" component. +It provides methods for starting and stopping. +When configuring the container, you essentially bridge the gap between an AMQP Queue and the `MessageListener` instance. +You must provide a reference to the `ConnectionFactory` and the queue names or Queue instances from which that listener should consume messages. + +Prior to version 2.0, there was one listener container, the `SimpleMessageListenerContainer`. +There is now a second container, the `DirectMessageListenerContainer`. +The differences between the containers and criteria you might apply when choosing which to use are described in xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container]. + +The following listing shows the most basic example, which works by using the, `SimpleMessageListenerContainer`: + +[source,java] +---- +SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); +container.setConnectionFactory(rabbitConnectionFactory); +container.setQueueNames("some.queue"); +container.setMessageListener(new MessageListenerAdapter(somePojo)); +---- + +As an "`active`" component, it is most common to create the listener container with a bean definition so that it can run in the background. +The following example shows one way to do so with XML: + +[source,xml] +---- + + + +---- + +The following listing shows another way to do so with XML: + +[source,xml] +---- + + + +---- + +Both of the preceding examples create a `DirectMessageListenerContainer` (notice the `type` attribute -- it defaults to `simple`). + +Alternately, you may prefer to use Java configuration, which looks similar to the preceding code snippet: + +[source,java] +---- +@Configuration +public class ExampleAmqpConfiguration { + + @Bean + public SimpleMessageListenerContainer messageListenerContainer() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(rabbitConnectionFactory()); + container.setQueueName("some.queue"); + container.setMessageListener(exampleListener()); + return container; + } + + @Bean + public CachingConnectionFactory rabbitConnectionFactory() { + CachingConnectionFactory connectionFactory = + new CachingConnectionFactory("localhost"); + connectionFactory.setUsername("guest"); + connectionFactory.setPassword("guest"); + return connectionFactory; + } + + @Bean + public MessageListener exampleListener() { + return new MessageListener() { + public void onMessage(Message message) { + System.out.println("received: " + message); + } + }; + } +} +---- + +[[consumer-priority]] +== Consumer Priority + +Starting with RabbitMQ Version 3.2, the broker now supports consumer priority (see https://www.rabbitmq.com/blog/2013/12/16/using-consumer-priorities-with-rabbitmq/[Using Consumer Priorities with RabbitMQ]). +This is enabled by setting the `x-priority` argument on the consumer. +The `SimpleMessageListenerContainer` now supports setting consumer arguments, as the following example shows: + +[source,java] +---- + +container.setConsumerArguments(Collections. + singletonMap("x-priority", Integer.valueOf(10))); +---- + +For convenience, the namespace provides the `priority` attribute on the `listener` element, as the following example shows: + +[source,xml] +---- + + + +---- + +Starting with version 1.3, you can modify the queues on which the container listens at runtime. +See xref:amqp/listener-queues.adoc#listener-queues[Listener Container Queues]. + +[[lc-auto-delete]] +== `auto-delete` Queues + +When a container is configured to listen to `auto-delete` queues, the queue has an `x-expires` option, or the https://www.rabbitmq.com/ttl.html[Time-To-Live] policy is configured on the Broker, the queue is removed by the broker when the container is stopped (that is, when the last consumer is cancelled). +Before version 1.3, the container could not be restarted because the queue was missing. +The `RabbitAdmin` only automatically redeclares queues and so on when the connection is closed or when it opens, which does not happen when the container is stopped and started. + +Starting with version 1.3, the container uses a `RabbitAdmin` to redeclare any missing queues during startup. + +You can also use conditional declaration (see xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration]) together with an `auto-startup="false"` admin to defer queue declaration until the container is started. +The following example shows how to do so: + +[source,xml] +---- + + + + + + + + + + + + + +---- + +In this case, the queue and exchange are declared by `containerAdmin`, which has `auto-startup="false"` so that the elements are not declared during context initialization. +Also, the container is not started for the same reason. +When the container is later started, it uses its reference to `containerAdmin` to declare the elements. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-returns.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-returns.adoc new file mode 100644 index 0000000000..a48bcd2fea --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-returns.adoc @@ -0,0 +1,24 @@ +[[async-returns]] += Asynchronous `@RabbitListener` Return Types +:page-section-summary-toc: 1 + +`@RabbitListener` (and `@RabbitHandler`) methods can be specified with asynchronous return types `CompletableFuture` and `Mono`, letting the reply be sent asynchronously. +`ListenableFuture` is no longer supported; it has been deprecated by Spring Framework. + +IMPORTANT: The listener container factory must be configured with `AcknowledgeMode.MANUAL` so that the consumer thread will not ack the message; instead, the asynchronous completion will ack or nack the message when the async operation completes. +When the async result is completed with an error, whether the message is requeued or not depends on the exception type thrown, the container configuration, and the container error handler. +By default, the message will be requeued, unless the container's `defaultRequeueRejected` property is set to `false` (it is `true` by default). +If the async result is completed with an `AmqpRejectAndDontRequeueException`, the message will not be requeued. +If the container's `defaultRequeueRejected` property is `false`, you can override that by setting the future's exception to a `ImmediateRequeueException` and the message will be requeued. +If some exception occurs within the listener method that prevents creation of the async result object, you MUST catch that exception and return an appropriate return object that will cause the message to be acknowledged or requeued. + +Starting with versions 2.2.21, 2.3.13, 2.4.1, the `AcknowledgeMode` will be automatically set the `MANUAL` when async return types are detected. +In addition, incoming messages with fatal exceptions will be negatively acknowledged individually, previously any prior unacknowledged message were also negatively acknowledged. + +Starting with version 3.0.5, the `@RabbitListener` (and `@RabbitHandler`) methods can be marked with Kotlin `suspend` and the whole handling process and reply producing (optional) happens on respective Kotlin coroutine. +All the mentioned rules about `AcknowledgeMode.MANUAL` are still apply. +The `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency must be present in classpath to allow `suspend` function invocations. + +Also starting with version 3.0.5, if a `RabbitListenerErrorHandler` is configured on a listener with an async return type (including Kotlin suspend functions), the error handler is invoked after a failure. +See xref:amqp/receiving-messages/async-annotation-driven/error-handling.adoc[Handling Exceptions] for more information about this error handler and its purpose. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc new file mode 100644 index 0000000000..5a6ae97838 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc @@ -0,0 +1,92 @@ +[[receiving-batch]] += @RabbitListener with Batching + +When receiving xref:amqp/sending-messages.adoc#template-batching[a batch] of messages, the de-batching is normally performed by the container, and the listener is invoked with one message at time. +Starting with version 2.2, you can configure the listener container factory and listener to receive the entire batch in one call, simply set the factory's `batchListener` property, and make the method payload parameter a `List` or `Collection`: + +[source, java] +---- +@Bean +public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + factory.setBatchListener(true); + return factory; +} + +@RabbitListener(queues = "batch.1") +public void listen1(List in) { + ... +} + +// or + +@RabbitListener(queues = "batch.2") +public void listen2(List> in) { + ... +} +---- + +Setting the `batchListener` property to true automatically turns off the `deBatchingEnabled` container property in containers that the factory creates (unless `consumerBatchEnabled` is `true` - see below). Effectively, the debatching is moved from the container to the listener adapter and the adapter creates the list that is passed to the listener. + +A batch-enabled factory cannot be used with a xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[multi-method listener]. + +Also starting with version 2.2. when receiving batched messages one-at-a-time, the last message contains a boolean header set to `true`. +This header can be obtained by adding the `@Header(AmqpHeaders.LAST_IN_BATCH)` boolean last` parameter to your listener method. +The header is mapped from `MessageProperties.isLastInBatch()`. +In addition, `AmqpHeaders.BATCH_SIZE` is populated with the size of the batch in every message fragment. + +In addition, a new property `consumerBatchEnabled` has been added to the `SimpleMessageListenerContainer`. +When this is true, the container will create a batch of messages, up to `batchSize`; a partial batch is delivered if `receiveTimeout` elapses with no new messages arriving. +If a producer-created batch is received, it is debatched and added to the consumer-side batch; therefore the actual number of messages delivered may exceed `batchSize`, which represents the number of messages received from the broker. +`deBatchingEnabled` must be true when `consumerBatchEnabled` is true; the container factory will enforce this requirement. + +[source, java] +---- +@Bean +public SimpleRabbitListenerContainerFactory consumerBatchContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(rabbitConnectionFactory()); + factory.setConsumerTagStrategy(consumerTagStrategy()); + factory.setBatchListener(true); // configures a BatchMessageListenerAdapter + factory.setBatchSize(2); + factory.setConsumerBatchEnabled(true); + return factory; +} +---- + +When using `consumerBatchEnabled` with `@RabbitListener`: + +[source, java] +---- +@RabbitListener(queues = "batch.1", containerFactory = "consumerBatchContainerFactory") +public void consumerBatch1(List amqpMessages) { + ... +} + +@RabbitListener(queues = "batch.2", containerFactory = "consumerBatchContainerFactory") +public void consumerBatch2(List> messages) { + ... +} + +@RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory") +public void consumerBatch3(List strings) { + ... +} +---- + +* the first is called with the raw, unconverted `org.springframework.amqp.core.Message` s received. +* the second is called with the `org.springframework.messaging.Message` s with converted payloads and mapped headers/properties. +* the third is called with the converted payloads, with no access to headers/properties. + +You can also add a `Channel` parameter, often used when using `MANUAL` ack mode. +This is not very useful with the third example because you don't have access to the `delivery_tag` property. + +Spring Boot provides a configuration property for `consumerBatchEnabled` and `batchSize`, but not for `batchListener`. +Starting with version 3.0, setting `consumerBatchEnabled` to `true` on the container factory also sets `batchListener` to `true`. +When `consumerBatchEnabled` is `true`, the listener **must** be a batch listener. + +Starting with version 3.0, listener methods can consume `Collection` or `List`. + +NOTE: The listener in batch mode does not support replies since there might not be a correlation between messages in the batch and single reply produced. +The xref:amqp/receiving-messages/async-returns.adoc[asynchronous return types] are still supported with batch listeners. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/choose-container.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/choose-container.adoc new file mode 100644 index 0000000000..78b3a6ccba --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/choose-container.adoc @@ -0,0 +1,36 @@ +[[choose-container]] += Choosing a Container + +Version 2.0 introduced the `DirectMessageListenerContainer` (DMLC). +Previously, only the `SimpleMessageListenerContainer` (SMLC) was available. +The SMLC uses an internal queue and a dedicated thread for each consumer. +If a container is configured to listen to multiple queues, the same consumer thread is used to process all the queues. +Concurrency is controlled by `concurrentConsumers` and other properties. +As messages arrive from the RabbitMQ client, the client thread hands them off to the consumer thread through the queue. +This architecture was required because, in early versions of the RabbitMQ client, multiple concurrent deliveries were not possible. +Newer versions of the client have a revised threading model and can now support concurrency. +This has allowed the introduction of the DMLC where the listener is now invoked directly on the RabbitMQ Client thread. +Its architecture is, therefore, actually "`simpler`" than the SMLC. +However, there are some limitations with this approach, and certain features of the SMLC are not available with the DMLC. +Also, concurrency is controlled by `consumersPerQueue` (and the client library's thread pool). +The `concurrentConsumers` and associated properties are not available with this container. + +The following features are available with the SMLC but not the DMLC: + +* `batchSize`: With the SMLC, you can set this to control how many messages are delivered in a transaction or to reduce the number of acks, but it may cause the number of duplicate deliveries to increase after a failure. +(The DMLC does have `messagesPerAck`, which you can use to reduce the acks, the same as with `batchSize` and the SMLC, but it cannot be used with transactions -- each message is delivered and ack'd in a separate transaction). +* `consumerBatchEnabled`: enables batching of discrete messages in the consumer; see xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. +* `maxConcurrentConsumers` and consumer scaling intervals or triggers -- there is no auto-scaling in the DMLC. +It does, however, let you programmatically change the `consumersPerQueue` property and the consumers are adjusted accordingly. + +However, the DMLC has the following benefits over the SMLC: + +* Adding and removing queues at runtime is more efficient. +With the SMLC, the entire consumer thread is restarted (all consumers canceled and re-created). +With the DMLC, unaffected consumers are not canceled. +* The context switch between the RabbitMQ Client thread and the consumer thread is avoided. +* Threads are shared across consumers rather than having a dedicated thread for each consumer in the SMLC. +However, see the IMPORTANT note about the connection factory configuration in xref:amqp/receiving-messages/threading.adoc[Threading and Asynchronous Consumers]. + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for information about which configuration properties apply to each container. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc new file mode 100644 index 0000000000..fe6bed849c --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc @@ -0,0 +1,39 @@ +[[consumer-events]] += Consumer Events + +The containers publish application events whenever a listener +(consumer) experiences a failure of some kind. +The event `ListenerContainerConsumerFailedEvent` has the following properties: + +* `container`: The listener container where the consumer experienced the problem. +* `reason`: A textual reason for the failure. +* `fatal`: A boolean indicating whether the failure was fatal. +With non-fatal exceptions, the container tries to restart the consumer, according to the `recoveryInterval` or `recoveryBackoff` (for the `SimpleMessageListenerContainer`) or the `monitorInterval` (for the `DirectMessageListenerContainer`). +* `throwable`: The `Throwable` that was caught. + +These events can be consumed by implementing `ApplicationListener`. + +NOTE: System-wide events (such as connection failures) are published by all consumers when `concurrentConsumers` is greater than 1. + +If a consumer fails because one if its queues is being used exclusively, by default, as well as publishing the event, a `DEBUG` log is issued (since 3.1, previously WARN). +To change this logging behavior, provide a custom `ConditionalExceptionLogger` in the `AbstractMessageListenerContainer` instance's `exclusiveConsumerExceptionLogger` property. +In addition, the `SimpleMessageListenerContainer` consumer restart after such an exception is now logged at DEBUG level by default (previously INFO). +A new method `logRestart()` has been added to the `ConditionalExceptionLogger` to allow this to be changed. + +Also, the `AbstractMessageListenerContainer.DefaultExclusiveConsumerLogger` is now public, allowing it to be sub classed. + +See also xref:amqp/connections.adoc#channel-close-logging[Logging Channel Close Events]. + +Fatal errors are always logged at the `ERROR` level. +This it not modifiable. + +Several other events are published at various stages of the container lifecycle: + +* `AsyncConsumerStartedEvent`: When the consumer is started. +* `AsyncConsumerRestartedEvent`: When the consumer is restarted after a failure - `SimpleMessageListenerContainer` only. +* `AsyncConsumerTerminatedEvent`: When a consumer is stopped normally. +* `AsyncConsumerStoppedEvent`: When the consumer is stopped - `SimpleMessageListenerContainer` only. +* `ConsumeOkEvent`: When a `consumeOk` is received from the broker, contains the queue name and `consumerTag` +* `ListenerContainerIdleEvent`: See xref:amqp/receiving-messages/idle-containers.adoc[Detecting Idle Asynchronous Consumers]. +* `MissingQueueEvent`: When a missing queue is detected. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumerTags.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumerTags.adoc new file mode 100644 index 0000000000..80cfbecabd --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumerTags.adoc @@ -0,0 +1,21 @@ +[[consumerTags]] += Consumer Tags +:page-section-summary-toc: 1 + +You can provide a strategy to generate consumer tags. +By default, the consumer tag is generated by the broker. +The following listing shows the `ConsumerTagStrategy` interface definition: + +[source,java] +---- +public interface ConsumerTagStrategy { + + String createConsumerTag(String queue); + +} +---- + +The queue is made available so that it can (optionally) be used in the tag. + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/de-batching.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/de-batching.adoc new file mode 100644 index 0000000000..30562937f0 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/de-batching.adoc @@ -0,0 +1,16 @@ +[[de-batching]] += Batched Messages +:page-section-summary-toc: 1 + +Batched messages (created by a producer) are automatically de-batched by listener containers (using the `springBatchFormat` message header). +Rejecting any message from a batch causes the entire batch to be rejected. +See xref:amqp/sending-messages.adoc#template-batching[Batching] for more information about batching. + +Starting with version 2.2, the `SimpleMessageListenerContainer` can be use to create batches on the consumer side (where the producer sent discrete messages). + +Set the container property `consumerBatchEnabled` to enable this feature. +`deBatchingEnabled` must also be true so that the container is responsible for processing batches of both types. +Implement `BatchMessageListener` or `ChannelAwareBatchMessageListener` when `consumerBatchEnabled` is true. +Starting with version 2.2.7 both the `SimpleMessageListenerContainer` and `DirectMessageListenerContainer` can debatch xref:amqp/sending-messages.adoc#template-batching[producer created batches] as `List`. +See xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching] for information about using this feature with `@RabbitListener`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/idle-containers.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/idle-containers.adoc new file mode 100644 index 0000000000..239fa97a2b --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/idle-containers.adoc @@ -0,0 +1,95 @@ +[[idle-containers]] += Detecting Idle Asynchronous Consumers + +While efficient, one problem with asynchronous consumers is detecting when they are idle -- users might want to take +some action if no messages arrive for some period of time. + +Starting with version 1.6, it is now possible to configure the listener container to publish a +`ListenerContainerIdleEvent` when some time passes with no message delivery. +While the container is idle, an event is published every `idleEventInterval` milliseconds. + +To configure this feature, set `idleEventInterval` on the container. +The following example shows how to do so in XML and in Java (for both a `SimpleMessageListenerContainer` and a `SimpleRabbitListenerContainerFactory`): + +[source, xml] +---- + + + +---- + +[source, java] +---- +@Bean +public SimpleMessageListenerContainer(ConnectionFactory connectionFactory) { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + ... + container.setIdleEventInterval(60000L); + ... + return container; +} +---- + +[source, java] +---- +@Bean +public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(rabbitConnectionFactory()); + factory.setIdleEventInterval(60000L); + ... + return factory; +} +---- + +In each of these cases, an event is published once per minute while the container is idle. + +[[event-consumption]] +== Event Consumption + +You can capture idle events by implementing `ApplicationListener` -- either a general listener, or one narrowed to only +receive this specific event. +You can also use `@EventListener`, introduced in Spring Framework 4.2. + +The following example combines the `@RabbitListener` and `@EventListener` into a single class. +You need to understand that the application listener gets events for all containers, so you may need to +check the listener ID if you want to take specific action based on which container is idle. +You can also use the `@EventListener` `condition` for this purpose. + +The events have four properties: + +* `source`: The listener container instance +* `id`: The listener ID (or container bean name) +* `idleTime`: The time the container had been idle when the event was published +* `queueNames`: The names of the queue(s) that the container listens to + +The following example shows how to create listeners by using both the `@RabbitListener` and the `@EventListener` annotations: + +[source, Java] +---- +public class Listener { + + @RabbitListener(id="someId", queues="#{queue.name}") + public String listen(String foo) { + return foo.toUpperCase(); + } + + @EventListener(condition = "event.listenerId == 'someId'") + public void onApplicationEvent(ListenerContainerIdleEvent event) { + ... + } + +} +---- + +IMPORTANT: Event listeners see events for all containers. +Consequently, in the preceding example, we narrow the events received based on the listener ID. + +CAUTION: If you wish to use the idle event to stop the lister container, you should not call `container.stop()` on the thread that calls the listener. +Doing so always causes delays and unnecessary log messages. +Instead, you should hand off the event to a different thread that can then stop the container. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc new file mode 100644 index 0000000000..bbd1c340ee --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc @@ -0,0 +1,20 @@ +[[micrometer-observation]] += Micrometer Observation +:page-section-summary-toc: 1 + +Using Micrometer for observation is now supported, since version 3.0, for the `RabbitTemplate` and listener containers. + +Set `observationEnabled` on each component to enable observation; this will disable xref:amqp/receiving-messages/micrometer.adoc[Micrometer Timers] because the timers will now be managed with each observation. +When using annotated listeners, set `observationEnabled` on the container factory. + +Refer to {micrometer-tracing-docs}[Micrometer Tracing] for more information. + +To add tags to timers/traces, configure a custom `RabbitTemplateObservationConvention` or `RabbitListenerObservationConvention` to the template or listener container, respectively. + +The default implementations add the `name` tag for template observations and `listener.id` tag for containers. + +You can either subclass `DefaultRabbitTemplateObservationConvention` or `DefaultRabbitListenerObservationConvention` or provide completely new implementations. + +See xref:appendix/micrometer.adoc[Micrometer Observation Documentation] for more details. + +WARNING: Due to ambiguity in how traces should be handled in a batch, observations are *NOT* created for xref:amqp/receiving-messages/batch.adoc[Batch Listener Containers]. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc new file mode 100644 index 0000000000..8ccb1548eb --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc @@ -0,0 +1,21 @@ +[[micrometer]] += Micrometer Integration +:page-section-summary-toc: 1 + +NOTE: This section documents the integration with {micrometer-docs}[Micrometer]. +For integration with Micrometer Observation, see xref:amqp/receiving-messages/micrometer-observation.adoc[Micrometer Observation]. + +Starting with version 2.2, the listener containers will automatically create and update Micrometer `Timer` s for the listener, if `Micrometer` is detected on the class path, and a single `MeterRegistry` is present in the application context (or exactly one is annotated `@Primary`, such as when using Spring Boot). +The timers can be disabled by setting the container property `micrometerEnabled` to `false`. + +Two timers are maintained - one for successful calls to the listener and one for failures. +With a simple `MessageListener`, there is a pair of timers for each configured queue. + +The timers are named `spring.rabbitmq.listener` and have the following tags: + +* `listenerId` : (listener id or container bean name) +* `queue` : (the queue name for a simple listener or list of configured queue names when `consumerBatchEnabled` is `true` - because a batch may contain messages from multiple queues) +* `result` : `success` or `failure` +* `exception` : `none` or `ListenerExecutionFailedException` + +You can add additional tags using the `micrometerTags` container property. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/polling-consumer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/polling-consumer.adoc new file mode 100644 index 0000000000..893ab1b141 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/polling-consumer.adoc @@ -0,0 +1,102 @@ +[[polling-consumer]] += Polling Consumer + +The `AmqpTemplate` itself can be used for polled `Message` reception. +By default, if no message is available, `null` is returned immediately. +There is no blocking. +Starting with version 1.5, you can set a `receiveTimeout`, in milliseconds, and the receive methods block for up to that long, waiting for a message. +A value less than zero means block indefinitely (or at least until the connection to the broker is lost). +Version 1.6 introduced variants of the `receive` methods that allows the timeout be passed in on each call. + +CAUTION: Since the receive operation creates a new `QueueingConsumer` for each message, this technique is not really appropriate for high-volume environments. +Consider using an asynchronous consumer or a `receiveTimeout` of zero for those use cases. + +Starting with version 2.4.8, when using a non-zero timeout, you can specify arguments passed into the `basicConsume` method used to associate the consumer with the channel. +For example: `template.addConsumerArg("x-priority", 10)`. + +There are four simple `receive` methods available. +As with the `Exchange` on the sending side, there is a method that requires that a default queue property has been set +directly on the template itself, and there is a method that accepts a queue parameter at runtime. +Version 1.6 introduced variants to accept `timeoutMillis` to override `receiveTimeout` on a per-request basis. +The following listing shows the definitions of the four methods: + +[source,java] +---- +Message receive() throws AmqpException; + +Message receive(String queueName) throws AmqpException; + +Message receive(long timeoutMillis) throws AmqpException; + +Message receive(String queueName, long timeoutMillis) throws AmqpException; +---- + +As in the case of sending messages, the `AmqpTemplate` has some convenience methods for receiving POJOs instead of `Message` instances, and implementations provide a way to customize the `MessageConverter` used to create the `Object` returned: +The following listing shows those methods: + +[source,java] +---- +Object receiveAndConvert() throws AmqpException; + +Object receiveAndConvert(String queueName) throws AmqpException; + +Object receiveAndConvert(long timeoutMillis) throws AmqpException; + +Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException; +---- + +Starting with version 2.0, there are variants of these methods that take an additional `ParameterizedTypeReference` argument to convert complex types. +The template must be configured with a `SmartMessageConverter`. +See xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`] for more information. + +Similar to `sendAndReceive` methods, beginning with version 1.3, the `AmqpTemplate` has several convenience `receiveAndReply` methods for synchronously receiving, processing and replying to messages. +The following listing shows those method definitions: + +[source,java] +---- + boolean receiveAndReply(ReceiveAndReplyCallback callback) + throws AmqpException; + + boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) + throws AmqpException; + + boolean receiveAndReply(ReceiveAndReplyCallback callback, + String replyExchange, String replyRoutingKey) throws AmqpException; + + boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, + String replyExchange, String replyRoutingKey) throws AmqpException; + + boolean receiveAndReply(ReceiveAndReplyCallback callback, + ReplyToAddressCallback replyToAddressCallback) throws AmqpException; + + boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, + ReplyToAddressCallback replyToAddressCallback) throws AmqpException; +---- + +The `AmqpTemplate` implementation takes care of the `receive` and `reply` phases. +In most cases, you should provide only an implementation of `ReceiveAndReplyCallback` to perform some business logic for the received message and build a reply object or message, if needed. +Note, a `ReceiveAndReplyCallback` may return `null`. +In this case, no reply is sent and `receiveAndReply` works like the `receive` method. +This lets the same queue be used for a mixture of messages, some of which may not need a reply. + +Automatic message (request and reply) conversion is applied only if the provided callback is not an instance of `ReceiveAndReplyMessageCallback`, which provides a raw message exchange contract. + +The `ReplyToAddressCallback` is useful for cases requiring custom logic to determine the `replyTo` address at runtime against the received message and reply from the `ReceiveAndReplyCallback`. +By default, `replyTo` information in the request message is used to route the reply. + +The following listing shows an example of POJO-based receive and reply: + +[source,java] +---- +boolean received = + this.template.receiveAndReply(ROUTE, new ReceiveAndReplyCallback() { + + public Invoice handle(Order order) { + return processOrder(order); + } + }); +if (received) { + log.info("We received an order!"); +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc new file mode 100644 index 0000000000..b19c58bfb0 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc @@ -0,0 +1,28 @@ +[[threading]] += Threading and Asynchronous Consumers + +A number of different threads are involved with asynchronous consumers. + +Threads from the `TaskExecutor` configured in the `SimpleMessageListenerContainer` are used to invoke the `MessageListener` when a new message is delivered by `RabbitMQ Client`. +If not configured, a `SimpleAsyncTaskExecutor` is used. +If you use a pooled executor, you need to ensure the pool size is sufficient to handle the configured concurrency. +With the `DirectMessageListenerContainer`, the `MessageListener` is invoked directly on a `RabbitMQ Client` thread. +In this case, the `taskExecutor` is used for the task that monitors the consumers. + +NOTE: When using the default `SimpleAsyncTaskExecutor`, for the threads the listener is invoked on, the listener container `beanName` is used in the `threadNamePrefix`. +This is useful for log analysis. +We generally recommend always including the thread name in the logging appender configuration. +When a `TaskExecutor` is specifically provided through the `taskExecutor` property on the container, it is used as is, without modification. +It is recommended that you use a similar technique to name the threads created by a custom `TaskExecutor` bean definition, to aid with thread identification in log messages. + +The `Executor` configured in the `CachingConnectionFactory` is passed into the `RabbitMQ Client` when creating the connection, and its threads are used to deliver new messages to the listener container. +If this is not configured, the client uses an internal thread pool executor with (at the time of writing) a pool size of `Runtime.getRuntime().availableProcessors() * 2` for each connection. + +If you have a large number of factories or are using `CacheMode.CONNECTION`, you may wish to consider using a shared `ThreadPoolTaskExecutor` with enough threads to satisfy your workload. + +IMPORTANT: With the `DirectMessageListenerContainer`, you need to ensure that the connection factory is configured with a task executor that has sufficient threads to support your desired concurrency across all listener containers that use that factory. +The default pool size (at the time of writing) is `Runtime.getRuntime().availableProcessors() * 2`. + +The `RabbitMQ client` uses a `ThreadFactory` to create threads for low-level I/O (socket) operations. +To modify this factory, you need to configure the underlying RabbitMQ `ConnectionFactory`, as discussed in xref:amqp/connections.adoc#connection-factory[Configuring the Underlying Client Connection Factory]. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/using-container-factories.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/using-container-factories.adoc new file mode 100644 index 0000000000..c2a2f00d0f --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/using-container-factories.adoc @@ -0,0 +1,51 @@ +[[using-container-factories]] += Using Container Factories + +Listener container factories were introduced to support the `@RabbitListener` and registering containers with the `RabbitListenerEndpointRegistry`, as discussed in xref:amqp/receiving-messages/async-annotation-driven/registration.adoc[Programmatic Endpoint Registration]. + +Starting with version 2.1, they can be used to create any listener container -- even a container without a listener (such as for use in Spring Integration). +Of course, a listener must be added before the container is started. + +There are two ways to create such containers: + +* Use a SimpleRabbitListenerEndpoint +* Add the listener after creation + +The following example shows how to use a `SimpleRabbitListenerEndpoint` to create a listener container: + +[source, java] +---- +@Bean +public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener( + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { + SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); + endpoint.setQueueNames("queue.1"); + endpoint.setMessageListener(message -> { + ... + }); + return rabbitListenerContainerFactory.createListenerContainer(endpoint); +} +---- + +The following example shows how to add the listener after creation: + +[source, java] +---- +@Bean +public SimpleMessageListenerContainer factoryCreatedContainerNoListener( + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { + SimpleMessageListenerContainer container = rabbitListenerContainerFactory.createListenerContainer(); + container.setMessageListener(message -> { + ... + }); + container.setQueueNames("test.no.listener.yet"); + return container; +} +---- + +In either case, the listener can also be a `ChannelAwareMessageListener`, since it is now a sub-interface of `MessageListener`. + +These techniques are useful if you wish to create several containers with similar properties or use a pre-configured container factory such as the one provided by Spring Boot auto configuration or both. + +IMPORTANT: Containers created this way are normal `@Bean` instances and are not registered in the `RabbitListenerEndpointRegistry`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc new file mode 100644 index 0000000000..e3a8634e95 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc @@ -0,0 +1,299 @@ +[[request-reply]] += Request/Reply Messaging + +The `AmqpTemplate` also provides a variety of `sendAndReceive` methods that accept the same argument options that were described earlier for the one-way send operations (`exchange`, `routingKey`, and `Message`). +Those methods are quite useful for request-reply scenarios, since they handle the configuration of the necessary `reply-to` property before sending and can listen for the reply message on an exclusive queue that is created internally for that purpose. + +Similar request-reply methods are also available where the `MessageConverter` is applied to both the request and reply. +Those methods are named `convertSendAndReceive`. +See the javadoc:org.springframework.amqp.core.AmqpTemplate[Javadoc of `AmqpTemplate`] for more detail. + +Starting with version 1.5.0, each of the `sendAndReceive` method variants has an overloaded version that takes `CorrelationData`. +Together with a properly configured connection factory, this enables the receipt of publisher confirms for the send side of the operation. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and the javadoc:org.springframework.amqp.rabbit.core.RabbitOperations[Javadoc for `RabbitOperations`] for more information. + +Starting with version 2.0, there are variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. +The template must be configured with a `SmartMessageConverter`. +See xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`] for more information. + +Starting with version 2.1, you can configure the `RabbitTemplate` with the `noLocalReplyConsumer` option to control a `noLocal` flag for reply consumers. +This is `false` by default. + +[[reply-timeout]] +== Reply Timeout + +By default, the send and receive methods timeout after five seconds and return null. +You can modify this behavior by setting the `replyTimeout` property. +Starting with version 1.5, if you set the `mandatory` property to `true` (or the `mandatory-expression` evaluates to `true` for a particular message), if the message cannot be delivered to a queue, an `AmqpMessageReturnedException` is thrown. +This exception has `returnedMessage`, `replyCode`, and `replyText` properties, as well as the `exchange` and `routingKey` used for the send. + +NOTE: This feature uses publisher returns. +You can enable it by setting `publisherReturns` to `true` on the `CachingConnectionFactory` (see xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns]). +Also, you must not have registered your own `ReturnCallback` with the `RabbitTemplate`. + +Starting with version 2.1.2, a `replyTimedOut` method has been added, letting subclasses be informed of the timeout so that they can clean up any retained state. + +Starting with versions 2.0.11 and 2.1.3, when you use the default `DirectReplyToMessageListenerContainer`, you can add an error handler by setting the template's `replyErrorHandler` property. +This error handler is invoked for any failed deliveries, such as late replies and messages received without a correlation header. +The exception passed in is a `ListenerExecutionFailedException`, which has a `failedMessage` property. + +[[direct-reply-to]] +== RabbitMQ Direct reply-to + +IMPORTANT: Starting with version 3.4.0, the RabbitMQ server supports https://www.rabbitmq.com/direct-reply-to.html[direct reply-to]. +This eliminates the main reason for a fixed reply queue (to avoid the need to create a temporary queue for each request). +Starting with Spring AMQP version 1.4.1 direct reply-to is used by default (if supported by the server) instead of creating temporary reply queues. +When no `replyQueue` is provided (or it is set with a name of `amq.rabbitmq.reply-to`), the `RabbitTemplate` automatically detects whether direct reply-to is supported and either uses it or falls back to using a temporary reply queue. +When using direct reply-to, a `reply-listener` is not required and should not be configured. + +Reply listeners are still supported with named queues (other than `amq.rabbitmq.reply-to`), allowing control of reply concurrency and so on. + +Starting with version 1.6, if you wish to use a temporary, exclusive, auto-delete queue for each +reply, set the `useTemporaryReplyQueues` property to `true`. +This property is ignored if you set a `replyAddress`. + +You can change the criteria that dictate whether to use direct reply-to by subclassing `RabbitTemplate` and overriding `useDirectReplyTo()` to check different criteria. +The method is called once only, when the first request is sent. + +Prior to version 2.0, the `RabbitTemplate` created a new consumer for each request and canceled the consumer when the reply was received (or timed out). +Now the template uses a `DirectReplyToMessageListenerContainer` instead, letting the consumers be reused. +The template still takes care of correlating the replies, so there is no danger of a late reply going to a different sender. +If you want to revert to the previous behavior, set the `useDirectReplyToContainer` (`direct-reply-to-container` when using XML configuration) property to false. + +The `AsyncRabbitTemplate` has no such option. +It always used a `DirectReplyToContainer` for replies when direct reply-to is used. + +Starting with version 2.3.7, the template has a new property `useChannelForCorrelation`. +When this is `true`, the server does not have to copy the correlation id from the request message headers to the reply message. +Instead, the channel used to send the request is used to correlate the reply to the request. + +[[message-correlation-with-a-reply-queue]] +== Message Correlation With A Reply Queue + +When using a fixed reply queue (other than `amq.rabbitmq.reply-to`), you must provide correlation data so that replies can be correlated to requests. +See https://www.rabbitmq.com/tutorials/tutorial-six-java.html[RabbitMQ Remote Procedure Call (RPC)]. +By default, the standard `correlationId` property is used to hold the correlation data. +However, if you wish to use a custom property to hold correlation data, you can set the `correlation-key` attribute on the . +Explicitly setting the attribute to `correlationId` is the same as omitting the attribute. +The client and server must use the same header for correlation data. + +NOTE: Spring AMQP version 1.1 used a custom property called `spring_reply_correlation` for this data. +If you wish to revert to this behavior with the current version (perhaps to maintain compatibility with another application using 1.1), you must set the attribute to `spring_reply_correlation`. + +By default, the template generates its own correlation ID (ignoring any user-supplied value). +If you wish to use your own correlation ID, set the `RabbitTemplate` instance's `userCorrelationId` property to `true`. + +IMPORTANT: The correlation ID must be unique to avoid the possibility of a wrong reply being returned for a request. + +[[reply-listener]] +== Reply Listener Container + +When using RabbitMQ versions prior to 3.4.0, a new temporary queue is used for each reply. +However, a single reply queue can be configured on the template, which can be more efficient and also lets you set arguments on that queue. +In this case, however, you must also provide a sub element. +This element provides a listener container for the reply queue, with the template being the listener. +All of the xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] attributes allowed on a are allowed on the element, except for `connection-factory` and `message-converter`, which are inherited from the template's configuration. + +IMPORTANT: If you run multiple instances of your application or use multiple `RabbitTemplate` instances, you *MUST* use a unique reply queue for each. +RabbitMQ has no ability to select messages from a queue, so, if they all use the same queue, each instance would compete for replies and not necessarily receive their own. + +The following example defines a rabbit template with a connection factory: + +[source,xml] +---- + + + +---- + +While the container and template share a connection factory, they do not share a channel. +Therefore, requests and replies are not performed within the same transaction (if transactional). + +NOTE: Prior to version 1.5.0, the `reply-address` attribute was not available. +Replies were always routed by using the default exchange and the `reply-queue` name as the routing key. +This is still the default, but you can now specify the new `reply-address` attribute. +The `reply-address` can contain an address with the form `/` and the reply is routed to the specified exchange and routed to a queue bound with the routing key. +The `reply-address` has precedence over `reply-queue`. +When only `reply-address` is in use, the `` must be configured as a separate `` component. +The `reply-address` and `reply-queue` (or `queues` attribute on the ``) must refer to the same queue logically. + +With this configuration, a `SimpleListenerContainer` is used to receive the replies, with the `RabbitTemplate` being the `MessageListener`. +When defining a template with the `` namespace element, as shown in the preceding example, the parser defines the container and wires in the template as the listener. + +NOTE: When the template does not use a fixed `replyQueue` (or is using direct reply-to -- see xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to]), a listener container is not needed. +Direct `reply-to` is the preferred mechanism when using RabbitMQ 3.4.0 or later. + +If you define your `RabbitTemplate` as a `` or use an `@Configuration` class to define it as an `@Bean` or when you create the template programmatically, you need to define and wire up the reply listener container yourself. +If you fail to do this, the template never receives the replies and eventually times out and returns null as the reply to a call to a `sendAndReceive` method. + +Starting with version 1.5, the `RabbitTemplate` detects if it has been +configured as a `MessageListener` to receive replies. +If not, attempts to send and receive messages with a reply address +fail with an `IllegalStateException` (because the replies are never received). + +Further, if a simple `replyAddress` (queue name) is used, the reply listener container verifies that it is listening +to a queue with the same name. +This check cannot be performed if the reply address is an exchange and routing key and a debug log message is written. + +IMPORTANT: When wiring the reply listener and template yourself, it is important to ensure that the template's `replyAddress` and the container's `queues` (or `queueNames`) properties refer to the same queue. +The template inserts the reply address into the outbound message `replyTo` property. + +The following listing shows examples of how to manually wire up the beans: + +[source,xml] +---- + + + + + + + + + + + + + + + + +---- + +[source,java] +---- + @Bean + public RabbitTemplate amqpTemplate() { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory()); + rabbitTemplate.setMessageConverter(msgConv()); + rabbitTemplate.setReplyAddress(replyQueue().getName()); + rabbitTemplate.setReplyTimeout(60000); + rabbitTemplate.setUseDirectReplyToContainer(false); + return rabbitTemplate; + } + + @Bean + public SimpleMessageListenerContainer replyListenerContainer() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(connectionFactory()); + container.setQueues(replyQueue()); + container.setMessageListener(amqpTemplate()); + return container; + } + + @Bean + public Queue replyQueue() { + return new Queue("my.reply.queue"); + } +---- + +A complete example of a `RabbitTemplate` wired with a fixed reply queue, together with a "`remote`" listener container that handles the request and returns the reply is shown in https://github.com/spring-projects/spring-amqp/tree/main/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java[this test case]. + +IMPORTANT: When the reply times out (`replyTimeout`), the `sendAndReceive()` methods return null. + +Prior to version 1.3.6, late replies for timed out messages were only logged. +Now, if a late reply is received, it is rejected (the template throws an `AmqpRejectAndDontRequeueException`). +If the reply queue is configured to send rejected messages to a dead letter exchange, the reply can be retrieved for later analysis. +To do so, bind a queue to the configured dead letter exchange with a routing key equal to the reply queue's name. + +See the https://www.rabbitmq.com/dlx.html[RabbitMQ Dead Letter Documentation] for more information about configuring dead lettering. +You can also take a look at the `FixedReplyQueueDeadLetterTests` test case for an example. + +[[async-template]] +== Async Rabbit Template + +Version 1.6 introduced the `AsyncRabbitTemplate`. +This has similar `sendAndReceive` (and `convertSendAndReceive`) methods to those on the xref:amqp/template.adoc[`AmqpTemplate`]. +However, instead of blocking, they return a `CompletableFuture`. + +The `sendAndReceive` methods return a `RabbitMessageFuture`. +The `convertSendAndReceive` methods return a `RabbitConverterFuture`. + +You can either synchronously retrieve the result later, by invoking `get()` on the future, or you can register a callback that is called asynchronously with the result. +The following listing shows both approaches: + +[source, java] +---- +@Autowired +private AsyncRabbitTemplate template; + +... + +public void doSomeWorkAndGetResultLater() { + + ... + + CompletableFuture future = this.template.convertSendAndReceive("foo"); + + // do some more work + + String reply = null; + try { + reply = future.get(10, TimeUnit.SECONDS); + } + catch (ExecutionException e) { + ... + } + + ... + +} + +public void doSomeWorkAndGetResultAsync() { + + ... + + RabbitConverterFuture future = this.template.convertSendAndReceive("foo"); + future.whenComplete((result, ex) -> { + if (ex == null) { + // success + } + else { + // failure + } + }); + + ... + +} +---- + +If `mandatory` is set and the message cannot be delivered, the future throws an `ExecutionException` with a cause of `AmqpMessageReturnedException`, which encapsulates the returned message and information about the return. + +If `enableConfirms` is set, the future has a property called `confirm`, which is itself a `CompletableFuture` with `true` indicating a successful publish. +If the confirm future is `false`, the `RabbitFuture` has a further property called `nackCause`, which contains the reason for the failure, if available. + +IMPORTANT: The publisher confirm is discarded if it is received after the reply, since the reply implies a successful publish. + +You can set the `receiveTimeout` property on the template to time out replies (it defaults to `30000` - 30 seconds). +If a timeout occurs, the future is completed with an `AmqpReplyTimeoutException`. + +The template implements `SmartLifecycle`. +Stopping the template while there are pending replies causes the pending `Future` instances to be canceled. + +Starting with version 2.0, the asynchronous template now supports https://www.rabbitmq.com/direct-reply-to.html[direct reply-to] instead of a configured reply queue. +To enable this feature, use one of the following constructors: + +[source, java] +---- +public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey) + +public AsyncRabbitTemplate(RabbitTemplate template) +---- + +See xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] to use direct reply-to with the synchronous `RabbitTemplate`. + +Version 2.0 introduced variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. +You must configure the underlying `RabbitTemplate` with a `SmartMessageConverter`. +See xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`] for more information. + +[[remoting]] +== Spring Remoting with AMQP + +Spring remoting is no longer supported because the functionality has been removed from Spring Framework. + +Use `sendAndReceive` operations using the `RabbitTemplate` (client side ) and `@RabbitListener` instead. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc b/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc new file mode 100644 index 0000000000..fb35a9723f --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc @@ -0,0 +1,249 @@ +[[resilience:-recovering-from-errors-and-broker-failures]] += Resilience: Recovering from Errors and Broker Failures + +Some of the key (and most popular) high-level features that Spring AMQP provides are to do with recovery and automatic re-connection in the event of a protocol error or broker failure. +We have seen all the relevant components already in this guide, but it should help to bring them all together here and call out the features and recovery scenarios individually. + +The primary reconnection features are enabled by the `CachingConnectionFactory` itself. +It is also often beneficial to use the `RabbitAdmin` auto-declaration features. +In addition, if you care about guaranteed delivery, you probably also need to use the `channelTransacted` flag in `RabbitTemplate` and `SimpleMessageListenerContainer` and the `AcknowledgeMode.AUTO` (or manual if you do the acks yourself) in the `SimpleMessageListenerContainer`. + +[[automatic-declaration]] +== Automatic Declaration of Exchanges, Queues, and Bindings + +The `RabbitAdmin` component can declare exchanges, queues, and bindings on startup. +It does this lazily, through a `ConnectionListener`. +Consequently, if the broker is not present on startup, it does not matter. +The first time a `Connection` is used (for example, +by sending a message) the listener fires and the admin features is applied. +A further benefit of doing the auto declarations in a listener is that, if the connection is dropped for any reason (for example, +broker death, network glitch, and others), they are applied again when the connection is re-established. + +NOTE: Queues declared this way must have fixed names -- either explicitly declared or generated by the framework for `AnonymousQueue` instances. +Anonymous queues are non-durable, exclusive, and auto-deleting. + +IMPORTANT: Automatic declaration is performed only when the `CachingConnectionFactory` cache mode is `CHANNEL` (the default). +This limitation exists because exclusive and auto-delete queues are bound to the connection. + +Starting with version 2.2.2, the `RabbitAdmin` will detect beans of type `DeclarableCustomizer` and apply the function before actually processing the declaration. +This is useful, for example, to set a new argument (property) before it has first class support within the framework. + +[source, java] +---- +@Bean +public DeclarableCustomizer customizer() { + return dec -> { + if (dec instanceof Queue && ((Queue) dec).getName().equals("my.queue")) { + dec.addArgument("some.new.queue.argument", true); + } + return dec; + }; +} +---- + +It is also useful in projects that don't provide direct access to the `Declarable` bean definitions. + +See also xref:amqp/connections.adoc#auto-recovery[RabbitMQ Automatic Connection/Topology recovery]. + +[[retry]] +== Failures in Synchronous Operations and Options for Retry + +If you lose your connection to the broker in a synchronous sequence when using `RabbitTemplate` (for instance), Spring AMQP throws an `AmqpException` (usually, but not always, `AmqpIOException`). +We do not try to hide the fact that there was a problem, so you have to be able to catch and respond to the exception. +The easiest thing to do if you suspect that the connection was lost (and it was not your fault) is to try the operation again. +You can do this manually, or you could look at using Spring Retry to handle the retry (imperatively or declaratively). + +Spring Retry provides a couple of AOP interceptors and a great deal of flexibility to specify the parameters of the retry (number of attempts, exception types, backoff algorithm, and others). +Spring AMQP also provides some convenience factory beans for creating Spring Retry interceptors in a convenient form for AMQP use cases, with strongly typed callback interfaces that you can use to implement custom recovery logic. +See the Javadoc and properties of `StatefulRetryOperationsInterceptor` and `StatelessRetryOperationsInterceptor` for more detail. +Stateless retry is appropriate if there is no transaction or if a transaction is started inside the retry callback. +Note that stateless retry is simpler to configure and analyze than stateful retry, but it is not usually appropriate if there is an ongoing transaction that must be rolled back or definitely is going to roll back. +A dropped connection in the middle of a transaction should have the same effect as a rollback. +Consequently, for reconnections where the transaction is started higher up the stack, stateful retry is usually the best choice. +Stateful retry needs a mechanism to uniquely identify a message. +The simplest approach is to have the sender put a unique value in the `MessageId` message property. +The provided message converters provide an option to do this: you can set `createMessageIds` to `true`. +Otherwise, you can inject a `MessageKeyGenerator` implementation into the interceptor. +The key generator must return a unique key for each message. +In versions prior to version 2.0, a `MissingMessageIdAdvice` was provided. +It enabled messages without a `messageId` property to be retried exactly once (ignoring the retry settings). +This advice is no longer provided, since, along with `spring-retry` version 1.2, its functionality is built into the interceptor and message listener containers. + +NOTE: For backwards compatibility, a message with a null message ID is considered fatal for the consumer (consumer is stopped) by default (after one retry). +To replicate the functionality provided by the `MissingMessageIdAdvice`, you can set the `statefulRetryFatalWithNullMessageId` property to `false` on the listener container. +With that setting, the consumer continues to run and the message is rejected (after one retry). +It is discarded or routed to the dead letter queue (if one is configured). + +Starting with version 1.3, a builder API is provided to aid in assembling these interceptors by using Java (in `@Configuration` classes). +The following example shows how to do so: + +[source,java] +---- +@Bean +public StatefulRetryOperationsInterceptor interceptor() { + return RetryInterceptorBuilder.stateful() + .maxAttempts(5) + .backOffOptions(1000, 2.0, 10000) // initialInterval, multiplier, maxInterval + .build(); +} +---- + +Only a subset of retry capabilities can be configured this way. +More advanced features would need the configuration of a `RetryTemplate` as a Spring bean. +See the {spring-retry-java-docs}[Spring Retry Javadoc] for complete information about available policies and their configuration. + +[[batch-retry]] +== Retry with Batch Listeners + +It is not recommended to configure retry with a batch listener, unless the batch was created by the producer, in a single record. +See xref:amqp/receiving-messages/de-batching.adoc[Batched Messages] for information about consumer and producer-created batches. +With a consumer-created batch, the framework has no knowledge about which message in the batch caused the failure so recovery after the retries are exhausted is not possible. +With producer-created batches, since there is only one message that actually failed, the whole message can be recovered. +Applications may want to inform a custom recoverer where in the batch the failure occurred, perhaps by setting an index property of the thrown exception. + +A retry recoverer for a batch listener must implement `MessageBatchRecoverer`. + +[[async-listeners]] +== Message Listeners and the Asynchronous Case + +If a `MessageListener` fails because of a business exception, the exception is handled by the message listener container, which then goes back to listening for another message. +If the failure is caused by a dropped connection (not a business exception), the consumer that is collecting messages for the listener has to be cancelled and restarted. +The `SimpleMessageListenerContainer` handles this seamlessly, and it leaves a log to say that the listener is being restarted. +In fact, it loops endlessly, trying to restart the consumer. +Only if the consumer is very badly behaved indeed will it give up. +One side effect is that if the broker is down when the container starts, it keeps trying until a connection can be established. + +Business exception handling, as opposed to protocol errors and dropped connections, might need more thought and some custom configuration, especially if transactions or container acks are in use. +Prior to 2.8.x, RabbitMQ had no definition of dead letter behavior. +Consequently, by default, a message that is rejected or rolled back because of a business exception can be redelivered endlessly. +To put a limit on the client on the number of re-deliveries, one choice is a `StatefulRetryOperationsInterceptor` in the advice chain of the listener. +The interceptor can have a recovery callback that implements a custom dead letter action -- whatever is appropriate for your particular environment. + +Another alternative is to set the container's `defaultRequeueRejected` property to `false`. +This causes all failed messages to be discarded. +When using RabbitMQ 2.8.x or higher, this also facilitates delivering the message to a dead letter exchange. + +Alternatively, you can throw a `AmqpRejectAndDontRequeueException`. +Doing so prevents message requeuing, regardless of the setting of the `defaultRequeueRejected` property. + +Starting with version 2.1, an `ImmediateRequeueAmqpException` is introduced to perform exactly the opposite logic: the message will be requeued, regardless of the setting of the `defaultRequeueRejected` property. + +Often, a combination of both techniques is used. +You can use a `StatefulRetryOperationsInterceptor` in the advice chain with a `MessageRecoverer` that throws an `AmqpRejectAndDontRequeueException`. +The `MessageRecover` is called when all retries have been exhausted. +The `RejectAndDontRequeueRecoverer` does exactly that. +The default `MessageRecoverer` consumes the errant message and emits a `WARN` message. + +Starting with version 1.3, a new `RepublishMessageRecoverer` is provided, to allow publishing of failed messages after retries are exhausted. + +When a recoverer consumes the final exception, the message is ack'd and is not sent to the dead letter exchange by the broker, if configured. + +NOTE: When `RepublishMessageRecoverer` is used on the consumer side, the received message has `deliveryMode` in the `receivedDeliveryMode` message property. +In this case the `deliveryMode` is `null`. +That means a `NON_PERSISTENT` delivery mode on the broker. +Starting with version 2.0, you can configure the `RepublishMessageRecoverer` for the `deliveryMode` to set into the message to republish if it is `null`. +By default, it uses `MessageProperties` default value - `MessageDeliveryMode.PERSISTENT`. + +The following example shows how to set a `RepublishMessageRecoverer` as the recoverer: + +[source,java] +---- +@Bean +RetryOperationsInterceptor interceptor() { + return RetryInterceptorBuilder.stateless() + .maxAttempts(5) + .recoverer(new RepublishMessageRecoverer(amqpTemplate(), "something", "somethingelse")) + .build(); +} +---- + +The `RepublishMessageRecoverer` publishes the message with additional information in message headers, such as the exception message, stack trace, original exchange, and routing key. +Additional headers can be added by creating a subclass and overriding `additionalHeaders()`. +The `deliveryMode` (or any other properties) can also be changed in the `additionalHeaders()`, as the following example shows: + +[source,java] +---- +RepublishMessageRecoverer recoverer = new RepublishMessageRecoverer(amqpTemplate, "error") { + + protected Map additionalHeaders(Message message, Throwable cause) { + message.getMessageProperties() + .setDeliveryMode(message.getMessageProperties().getReceivedDeliveryMode()); + return null; + } + +}; +---- + +Starting with version 2.0.5, the stack trace may be truncated if it is too large; this is because all headers have to fit in a single frame. +By default, if the stack trace would cause less than 20,000 bytes ('headroom') to be available for other headers, it will be truncated. +This can be adjusted by setting the recoverer's `frameMaxHeadroom` property, if you need more or less space for other headers. +Starting with versions 2.1.13, 2.2.3, the exception message is included in this calculation, and the amount of stack trace will be maximized using the following algorithm: + +* if the stack trace alone would exceed the limit, the exception message header will be truncated to 97 bytes plus `...` and the stack trace is truncated too. +* if the stack trace is small, the message will be truncated (plus `...`) to fit in the available bytes (but the message within the stack trace itself is truncated to 97 bytes plus `...`). + +Whenever a truncation of any kind occurs, the original exception will be logged to retain the complete information. +The evaluation is performed after the headers are enhanced so information such as the exception type can be used in the expressions. + +Starting with version 2.4.8, the error exchange and routing key can be provided as SpEL expressions, with the `Message` being the root object for the evaluation. + +Starting with version 2.3.3, a new subclass `RepublishMessageRecovererWithConfirms` is provided; this supports both styles of publisher confirms and will wait for the confirmation before returning (or throw an exception if not confirmed or the message is returned). + +If the confirm type is `CORRELATED`, the subclass will also detect if a message is returned and throw an `AmqpMessageReturnedException`; if the publication is negatively acknowledged, it will throw an `AmqpNackReceivedException`. + +If the confirm type is `SIMPLE`, the subclass will invoke the `waitForConfirmsOrDie` method on the channel. + +See xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns] for more information about confirms and returns. + +Starting with version 2.1, an `ImmediateRequeueMessageRecoverer` is added to throw an `ImmediateRequeueAmqpException`, which notifies a listener container to requeue the current failed message. + +[[exception-classification-for-spring-retry]] +== Exception Classification for Spring Retry + +Spring Retry has a great deal of flexibility for determining which exceptions can invoke retry. +The default configuration retries for all exceptions. +Given that user exceptions are wrapped in a `ListenerExecutionFailedException`, we need to ensure that the classification examines the exception causes. +The default classifier looks only at the top level exception. + +Since Spring Retry 1.0.3, the `BinaryExceptionClassifier` has a property called `traverseCauses` (default: `false`). +When `true`, it travers exception causes until it finds a match or there is no cause. + +To use this classifier for retry, you can use a `SimpleRetryPolicy` created with the constructor that takes the max attempts, the `Map` of `Exception` instances, and the boolean (`traverseCauses`) and inject this policy into the `RetryTemplate`. + +[[retry-over-broker]] +== Retry Over Broker + +The message dead-lettered from the queue can be republished back to this queue after re-routing from a DLX. +This retry behaviour is controlled on the broker side via an `x-death` header. +More information about this approach in the official https://www.rabbitmq.com/docs/dlx[RabbitMQ documentation]. + +The other approach is to re-publish failed message back to the original exchange manually from the application. +Starting with version `4.0`, the RabbitMQ broker does not consider `x-death` header sent from the client. +Essentially, any `x-*` headers are ignored from the client. + +To mitigate this new behavior of the RabbitMQ broker, Spring AMQP has introduced a `retry_count` header starting with version 3.2. +When this header is absent and a server side DLX is in action, the `x-death.count` property is mapped to this header. +When the failed message is re-published manually for retries, the `retry_count` header value has to be incremented manually. +See `MessageProperties.incrementRetryCount()` JavaDocs for more information. + +The following example summarise an algorithm for manual retry over the broker: + +[source,java] +---- +@RabbitListener(queueNames = "some_queue") +public void rePublish(Message message) { + try { + // Process message + } + catch (Exception ex) { + Long retryCount = message.getMessageProperties().getRetryCount(); + if (retryCount < 3) { + message.getMessageProperties().incrementRetryCount(); + this.rabbitTemplate.send("", "some_queue", message); + } + else { + throw new ImmediateAcknowledgeAmqpException("Failed after 4 attempts"); + } + } +} +---- diff --git a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc new file mode 100644 index 0000000000..eff9bd900f --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc @@ -0,0 +1,224 @@ +[[sending-messages]] += Sending Messages + +When sending a message, you can use any of the following methods: + +[source,java] +---- +void send(Message message) throws AmqpException; + +void send(String routingKey, Message message) throws AmqpException; + +void send(String exchange, String routingKey, Message message) throws AmqpException; +---- + +We can begin our discussion with the last method in the preceding listing, since it is actually the most explicit. +It lets an AMQP exchange name (along with a routing key) be provided at runtime. +The last parameter is the callback that is responsible for actual creating the message instance. +An example of using this method to send a message might look like this: +The following example shows how to use the `send` method to send a message: + +[source,java] +---- +amqpTemplate.send("marketData.topic", "quotes.nasdaq.THING1", + new Message("12.34".getBytes(), someProperties)); +---- + +You can set the `exchange` property on the template itself if you plan to use that template instance to send to the same exchange most or all of the time. +In such cases, you can use the second method in the preceding listing. +The following example is functionally equivalent to the previous example: + +[source,java] +---- +amqpTemplate.setExchange("marketData.topic"); +amqpTemplate.send("quotes.nasdaq.FOO", new Message("12.34".getBytes(), someProperties)); +---- + +If both the `exchange` and `routingKey` properties are set on the template, you can use the method that accepts only the `Message`. +The following example shows how to do so: + +[source,java] +---- +amqpTemplate.setExchange("marketData.topic"); +amqpTemplate.setRoutingKey("quotes.nasdaq.FOO"); +amqpTemplate.send(new Message("12.34".getBytes(), someProperties)); +---- + +A better way of thinking about the exchange and routing key properties is that the explicit method parameters always override the template's default values. +In fact, even if you do not explicitly set those properties on the template, there are always default values in place. +In both cases, the default is an empty `String`, but that is actually a sensible default. +As far as the routing key is concerned, it is not always necessary in the first place (for example, for +a `Fanout` exchange). +Furthermore, a queue may be bound to an exchange with an empty `String`. +Those are both legitimate scenarios for reliance on the default empty `String` value for the routing key property of the template. +As far as the exchange name is concerned, the empty `String` is commonly used because the AMQP specification defines the "`default exchange`" as having no name. +Since all queues are automatically bound to that default exchange (which is a direct exchange), using their name as the binding value, the second method in the preceding listing can be used for simple point-to-point messaging to any queue through the default exchange. +You can provide the queue name as the `routingKey`, either by providing the method parameter at runtime. +The following example shows how to do so: + +[source,java] +---- +RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange +template.send("queue.helloWorld", new Message("Hello World".getBytes(), someProperties)); +---- + +Alternately, you can create a template that can be used for publishing primarily or exclusively to a single Queue. +The following example shows how to do so: + +[source,java] +---- +RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange +template.setRoutingKey("queue.helloWorld"); // but we'll always send to this Queue +template.send(new Message("Hello World".getBytes(), someProperties)); +---- + +[[message-builder]] +== Message Builder API + +Starting with version 1.3, a message builder API is provided by the `MessageBuilder` and `MessagePropertiesBuilder`. +These methods provide a convenient "`fluent`" means of creating a message or message properties. +The following examples show the fluent API in action: + +[source,java] +---- +Message message = MessageBuilder.withBody("foo".getBytes()) + .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) + .setMessageId("123") + .setHeader("bar", "baz") + .build(); +---- + +[source,java] +---- +MessageProperties props = MessagePropertiesBuilder.newInstance() + .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) + .setMessageId("123") + .setHeader("bar", "baz") + .build(); +Message message = MessageBuilder.withBody("foo".getBytes()) + .andProperties(props) + .build(); +---- + +Each of the properties defined on the javadoc:org.springframework.amqp.core.MessageProperties[] can be set. +Other methods include `setHeader(String key, String value)`, `removeHeader(String key)`, `removeHeaders()`, and `copyProperties(MessageProperties properties)`. +Each property setting method has a `set*IfAbsent()` variant. +In the cases where a default initial value exists, the method is named `set*IfAbsentOrDefault()`. + +Five static methods are provided to create an initial message builder: + +[source,java] +---- +public static MessageBuilder withBody(byte[] body) <1> + +public static MessageBuilder withClonedBody(byte[] body) <2> + +public static MessageBuilder withBody(byte[] body, int from, int to) <3> + +public static MessageBuilder fromMessage(Message message) <4> + +public static MessageBuilder fromClonedMessage(Message message) <5> +---- + +<1> The message created by the builder has a body that is a direct reference to the argument. +<2> The message created by the builder has a body that is a new array containing a copy of bytes in the argument. +<3> The message created by the builder has a body that is a new array containing the range of bytes from the argument. +See https://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html[`Arrays.copyOfRange()`] for more details. +<4> The message created by the builder has a body that is a direct reference to the body of the argument. +The argument's properties are copied to a new `MessageProperties` object. +<5> The message created by the builder has a body that is a new array containing a copy of the argument's body. +The argument's properties are copied to a new `MessageProperties` object. + +Three static methods are provided to create a `MessagePropertiesBuilder` instance: + +[source,java] +---- +public static MessagePropertiesBuilder newInstance() <1> + +public static MessagePropertiesBuilder fromProperties(MessageProperties properties) <2> + +public static MessagePropertiesBuilder fromClonedProperties(MessageProperties properties) <3> +---- + +<1> A new message properties object is initialized with default values. +<2> The builder is initialized with, and `build()` will return, the provided properties object., +<3> The argument's properties are copied to a new `MessageProperties` object. + +With the `RabbitTemplate` implementation of `AmqpTemplate`, each of the `send()` methods has an overloaded version that takes an additional `CorrelationData` object. +When publisher confirms are enabled, this object is returned in the callback described in xref:amqp/template.adoc[`AmqpTemplate`]. +This lets the sender correlate a confirm (`ack` or `nack`) with the sent message. + +Starting with version 1.6.7, the `CorrelationAwareMessagePostProcessor` interface was introduced, allowing the correlation data to be modified after the message has been converted. +The following example shows how to use it: + +[source, java] +---- +Message postProcessMessage(Message message, Correlation correlation); +---- + +In version 2.0, this interface is deprecated. +The method has been moved to `MessagePostProcessor` with a default implementation that delegates to `postProcessMessage(Message message)`. + +Also starting with version 1.6.7, a new callback interface called `CorrelationDataPostProcessor` is provided. +This is invoked after all `MessagePostProcessor` instances (provided in the `send()` method as well as those provided in `setBeforePublishPostProcessors()`). +Implementations can update or replace the correlation data supplied in the `send()` method (if any). +The `Message` and original `CorrelationData` (if any) are provided as arguments. +The following example shows how to use the `postProcess` method: + +[source, java] +---- +CorrelationData postProcess(Message message, CorrelationData correlationData); +---- + +[[publisher-returns]] +== Publisher Returns + +When the template's `mandatory` property is `true`, returned messages are provided by the callback described in xref:amqp/template.adoc[`AmqpTemplate`]. + +Starting with version 1.4, the `RabbitTemplate` supports the SpEL `mandatoryExpression` property, which is evaluated against each request message as the root evaluation object, resolving to a `boolean` value. +Bean references, such as `@myBean.isMandatory(#root)`, can be used in the expression. + +Publisher returns can also be used internally by the `RabbitTemplate` in send and receive operations. +See xref:amqp/request-reply.adoc#reply-timeout[Reply Timeout] for more information. + +[[template-batching]] +== Batching + +Version 1.4.2 introduced the `BatchingRabbitTemplate`. +This is a subclass of `RabbitTemplate` with an overridden `send` method that batches messages according to the `BatchingStrategy`. +Only when a batch is complete is the message sent to RabbitMQ. +The following listing shows the `BatchingStrategy` interface definition: + +[source, java] +---- +public interface BatchingStrategy { + + MessageBatch addToBatch(String exchange, String routingKey, Message message); + + Date nextRelease(); + + Collection releaseBatches(); + +} +---- + +CAUTION: Batched data is held in memory. +Unsent messages can be lost in the event of a system failure. + +A `SimpleBatchingStrategy` is provided. +It supports sending messages to a single exchange or routing key. +It has the following properties: + +* `batchSize`: The number of messages in a batch before it is sent. +* `bufferLimit`: The maximum size of the batched message. +This preempts the `batchSize`, if exceeded, and causes a partial batch to be sent. +* `timeout`: A time after which a partial batch is sent when there is no new activity adding messages to the batch. + +The `SimpleBatchingStrategy` formats the batch by preceding each embedded message with a four-byte binary length. +This is communicated to the receiving system by setting the `springBatchFormat` message property to `lengthHeader4`. + +IMPORTANT: Batched messages are automatically de-batched by listener containers by default (by using the `springBatchFormat` message header). +Rejecting any message from a batch causes the entire batch to be rejected. + +However, see xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/template.adoc b/src/reference/antora/modules/ROOT/pages/amqp/template.adoc new file mode 100644 index 0000000000..0dea17c5d3 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/template.adoc @@ -0,0 +1,547 @@ +[[amqp-template]] += `AmqpTemplate` + +As with many other high-level abstractions provided by the Spring Framework and related projects, Spring AMQP provides a "`template`" that plays a central role. +The interface that defines the main operations is called `AmqpTemplate`. +Those operations cover the general behavior for sending and receiving messages. +In other words, they are not unique to any implementation -- hence the "`AMQP`" in the name. +On the other hand, there are implementations of that interface that are tied to implementations of the AMQP protocol. +Unlike JMS, which is an interface-level API itself, AMQP is a wire-level protocol. +The implementations of that protocol provide their own client libraries, so each implementation of the template interface depends on a particular client library. +Currently, there is only a single implementation: `RabbitTemplate`. +In the examples that follow, we often use an `AmqpTemplate`. +However, when you look at the configuration examples or any code excerpts where the template is instantiated or setters are invoked, you can see the implementation type (for example, `RabbitTemplate`). + +As mentioned earlier, the `AmqpTemplate` interface defines all the basic operations for sending and receiving messages. +We will explore message sending and reception, respectively, in xref:amqp/sending-messages.adoc#sending-messages[Sending Messages] and xref:amqp/receiving-messages.adoc#receiving-messages[Receiving Messages]. + +See also xref:amqp/request-reply.adoc#async-template[Async Rabbit Template]. + +[[template-retry]] +== Adding Retry Capabilities + +Starting with version 1.3, you can now configure the `RabbitTemplate` to use a `RetryTemplate` to help with handling problems with broker connectivity. +See the https://github.com/spring-projects/spring-retry[spring-retry] project for complete information. +The following is only one example that uses an exponential back off policy and the default `SimpleRetryPolicy`, which makes three tries before throwing the exception to the caller. + +The following example uses the XML namespace: + +[source,xml] +---- + + + + + + + + + + + +---- + +The following example uses the `@Configuration` annotation in Java: + +[source,java] +---- +@Bean +public RabbitTemplate rabbitTemplate() { + RabbitTemplate template = new RabbitTemplate(connectionFactory()); + RetryTemplate retryTemplate = new RetryTemplate(); + ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); + backOffPolicy.setInitialInterval(500); + backOffPolicy.setMultiplier(10.0); + backOffPolicy.setMaxInterval(10000); + retryTemplate.setBackOffPolicy(backOffPolicy); + template.setRetryTemplate(retryTemplate); + return template; +} +---- + +Starting with version 1.4, in addition to the `retryTemplate` property, the `recoveryCallback` option is supported on the `RabbitTemplate`. +It is used as a second argument for the `RetryTemplate.execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback)`. + +NOTE: The `RecoveryCallback` is somewhat limited, in that the retry context contains only the `lastThrowable` field. +For more sophisticated use cases, you should use an external `RetryTemplate` so that you can convey additional information to the `RecoveryCallback` through the context's attributes. +The following example shows how to do so: + +[source,java] +---- +retryTemplate.execute( + new RetryCallback() { + + @Override + public Object doWithRetry(RetryContext context) throws Exception { + context.setAttribute("message", message); + return rabbitTemplate.convertAndSend(exchange, routingKey, message); + } + + }, new RecoveryCallback() { + + @Override + public Object recover(RetryContext context) throws Exception { + Object message = context.getAttribute("message"); + Throwable t = context.getLastThrowable(); + // Do something with message + return null; + } + }); +} +---- + +In this case, you would *not* inject a `RetryTemplate` into the `RabbitTemplate`. + +[[publishing-is-async]] +== Publishing is Asynchronous -- How to Detect Successes and Failures + +Publishing messages is an asynchronous mechanism and, by default, messages that cannot be routed are dropped by RabbitMQ. +For successful publishing, you can receive an asynchronous confirm, as described in xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. +Consider two failure scenarios: + +* Publish to an exchange but there is no matching destination queue. +* Publish to a non-existent exchange. + +The first case is covered by publisher returns, as described in xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. + +For the second case, the message is dropped and no return is generated. +The underlying channel is closed with an exception. +By default, this exception is logged, but you can register a `ChannelListener` with the `CachingConnectionFactory` to obtain notifications of such events. +The following example shows how to add a `ConnectionListener`: + +[source, java] +---- +this.connectionFactory.addConnectionListener(new ConnectionListener() { + + @Override + public void onCreate(Connection connection) { + } + + @Override + public void onShutDown(ShutdownSignalException signal) { + ... + } + +}); +---- + +You can examine the signal's `reason` property to determine the problem that occurred. + +To detect the exception on the sending thread, you can `setChannelTransacted(true)` on the `RabbitTemplate` and the exception is detected on the `txCommit()`. +However, *transactions significantly impede performance*, so consider this carefully before enabling transactions for just this one use case. + +[[template-confirms]] +== Correlated Publisher Confirms and Returns + +The `RabbitTemplate` implementation of `AmqpTemplate` supports publisher confirms and returns. + +For returned messages, the template's `mandatory` property must be set to `true` or the `mandatory-expression` +must evaluate to `true` for a particular message. +This feature requires a `CachingConnectionFactory` that has its `publisherReturns` property set to `true` (see xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns]). +Returns are sent to the client by it registering a `RabbitTemplate.ReturnsCallback` by calling `setReturnsCallback(ReturnsCallback callback)`. +The callback must implement the following method: + +[source,java] +---- +void returnedMessage(ReturnedMessage returned); +---- + +The `ReturnedMessage` has the following properties: + +- `message` - the returned message itself +- `replyCode` - a code indicating the reason for the return +- `replyText` - a textual reason for the return - e.g. `NO_ROUTE` +- `exchange` - the exchange to which the message was sent +- `routingKey` - the routing key that was used + +Only one `ReturnsCallback` is supported by each `RabbitTemplate`. +See also xref:amqp/request-reply.adoc#reply-timeout[Reply Timeout]. + +For publisher confirms (also known as publisher acknowledgements), the template requires a `CachingConnectionFactory` that has its `publisherConfirm` property set to `ConfirmType.CORRELATED`. +Confirms are sent to the client by it registering a `RabbitTemplate.ConfirmCallback` by calling `setConfirmCallback(ConfirmCallback callback)`. +The callback must implement this method: + +[source,java] +---- +void confirm(CorrelationData correlationData, boolean ack, String cause); +---- + +The `CorrelationData` is an object supplied by the client when sending the original message. +The `ack` is true for an `ack` and false for a `nack`. +For `nack` instances, the cause may contain a reason for the `nack`, if it is available when the `nack` is generated. +An example is when sending a message to a non-existent exchange. +In that case, the broker closes the channel. +The reason for the closure is included in the `cause`. +The `cause` was added in version 1.4. + +Only one `ConfirmCallback` is supported by a `RabbitTemplate`. + +NOTE: When a rabbit template send operation completes, the channel is closed. +This precludes the reception of confirms or returns when the connection factory cache is full (when there is space in the cache, the channel is not physically closed and the returns and confirms proceed normally). +When the cache is full, the framework defers the close for up to five seconds, in order to allow time for the confirms and returns to be received. +When using confirms, the channel is closed when the last confirm is received. +When using only returns, the channel remains open for the full five seconds. +We generally recommend setting the connection factory's `channelCacheSize` to a large enough value so that the channel on which a message is published is returned to the cache instead of being closed. +You can monitor channel usage by using the RabbitMQ management plugin. +If you see channels being opened and closed rapidly, you should consider increasing the cache size to reduce overhead on the server. + +IMPORTANT: Before version 2.1, channels enabled for publisher confirms were returned to the cache before the confirms were received. +Some other process could check out the channel and perform some operation that causes the channel to close -- such as publishing a message to a non-existent exchange. +This could cause the confirm to be lost. +Version 2.1 and later no longer return the channel to the cache while confirms are outstanding. +The `RabbitTemplate` performs a logical `close()` on the channel after each operation. +In general, this means that only one confirm is outstanding on a channel at a time. + +NOTE: Starting with version 2.2, the callbacks are invoked on one of the connection factory's `executor` threads. +This is to avoid a potential deadlock if you perform Rabbit operations from within the callback. +With previous versions, the callbacks were invoked directly on the `amqp-client` connection I/O thread; this would deadlock if you perform some RPC operation (such as opening a new channel) since the I/O thread blocks waiting for the result, but the result needs to be processed by the I/O thread itself. +With those versions, it was necessary to hand off work (such as sending a message) to another thread within the callback. +This is no longer necessary since the framework now hands off the callback invocation to the executor. + +IMPORTANT: The guarantee of receiving a returned message before the ack is still maintained as long as the return callback executes in 60 seconds or less. +The confirm is scheduled to be delivered after the return callback exits or after 60 seconds, whichever comes first. + +The `CorrelationData` object has a `CompletableFuture` that you can use to get the result, instead of using a `ConfirmCallback` on the template. +The following example shows how to configure a `CorrelationData` instance: + +[source, java] +---- +CorrelationData cd1 = new CorrelationData(); +this.templateWithConfirmsEnabled.convertAndSend("exchange", queue.getName(), "foo", cd1); +assertTrue(cd1.getFuture().get(10, TimeUnit.SECONDS).isAck()); +ReturnedMessage = cd1.getReturn(); +... +---- + +Since it is a `CompletableFuture`, you can either `get()` the result when ready or use `whenComplete()` for an asynchronous callback. +The `Confirm` object is a simple bean with 2 properties: `ack` and `reason` (for `nack` instances). +The reason is not populated for broker-generated `nack` instances. +It is populated for `nack` instances generated by the framework (for example, closing the connection while `ack` instances are outstanding). + +In addition, when both confirms and returns are enabled, the `CorrelationData` `return` property is populated with the returned message, if it couldn't be routed to any queue. +It is guaranteed that the returned message property is set before the future is set with the `ack`. +`CorrelationData.getReturn()` returns a `ReturnMessage` with properties: + +* message (the returned message) +* replyCode +* replyText +* exchange +* routingKey + +See also xref:amqp/template.adoc#scoped-operations[Scoped Operations] for a simpler mechanism for waiting for publisher confirms. + +[[scoped-operations]] +== Scoped Operations + +Normally, when using the template, a `Channel` is checked out of the cache (or created), used for the operation, and returned to the cache for reuse. +In a multi-threaded environment, there is no guarantee that the next operation uses the same channel. +There may be times, however, where you want to have more control over the use of a channel and ensure that a number of operations are all performed on the same channel. + +Starting with version 2.0, a new method called `invoke` is provided, with an `OperationsCallback`. +Any operations performed within the scope of the callback and on the provided `RabbitOperations` argument use the same dedicated `Channel`, which will be closed at the end (not returned to a cache). +If the channel is a `PublisherCallbackChannel`, it is returned to the cache after all confirms have been received (see xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]). + +[source, java] +---- +@FunctionalInterface +public interface OperationsCallback { + + T doInRabbit(RabbitOperations operations); + +} +---- + +One example of why you might need this is if you wish to use the `waitForConfirms()` method on the underlying `Channel`. +This method was not previously exposed by the Spring API because the channel is, generally, cached and shared, as discussed earlier. +The `RabbitTemplate` now provides `waitForConfirms(long timeout)` and `waitForConfirmsOrDie(long timeout)`, which delegate to the dedicated channel used within the scope of the `OperationsCallback`. +The methods cannot be used outside of that scope, for obvious reasons. + +Note that a higher-level abstraction that lets you correlate confirms to requests is provided elsewhere (see xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]). +If you want only to wait until the broker has confirmed delivery, you can use the technique shown in the following example: + +[source, java] +---- +Collection messages = getMessagesToSend(); +Boolean result = this.template.invoke(t -> { + messages.forEach(m -> t.convertAndSend(ROUTE, m)); + t.waitForConfirmsOrDie(10_000); + return true; +}); +---- + +If you wish `RabbitAdmin` operations to be invoked on the same channel within the scope of the `OperationsCallback`, the admin must have been constructed by using the same `RabbitTemplate` that was used for the `invoke` operation. + +NOTE: The preceding discussion is moot if the template operations are already performed within the scope of an existing transaction -- for example, when running on a transacted listener container thread and performing operations on a transacted template. +In that case, the operations are performed on that channel and committed when the thread returns to the container. +It is not necessary to use `invoke` in that scenario. + +When using confirms in this way, much of the infrastructure set up for correlating confirms to requests is not really needed (unless returns are also enabled). +Starting with version 2.2, the connection factory supports a new property called `publisherConfirmType`. +When this is set to `ConfirmType.SIMPLE`, the infrastructure is avoided and the confirm processing can be more efficient. + +Furthermore, the `RabbitTemplate` sets the `publisherSequenceNumber` property in the sent message `MessageProperties`. +If you wish to check (or log or otherwise use) specific confirms, you can do so with an overloaded `invoke` method, as the following example shows: + +[source, java] +---- +public T invoke(OperationsCallback action, com.rabbitmq.client.ConfirmCallback acks, + com.rabbitmq.client.ConfirmCallback nacks); +---- + +NOTE: These `ConfirmCallback` objects (for `ack` and `nack` instances) are the Rabbit client callbacks, not the template callback. + +The following example logs `ack` and `nack` instances: + +[source, java] +---- +Collection messages = getMessagesToSend(); +Boolean result = this.template.invoke(t -> { + messages.forEach(m -> t.convertAndSend(ROUTE, m)); + t.waitForConfirmsOrDie(10_000); + return true; +}, (tag, multiple) -> { + log.info("Ack: " + tag + ":" + multiple); +}, (tag, multiple) -> { + log.info("Nack: " + tag + ":" + multiple); +})); +---- + +IMPORTANT: Scoped operations are bound to a thread. +See xref:amqp/template.adoc#multi-strict[Strict Message Ordering in a Multi-Threaded Environment] for a discussion about strict ordering in a multi-threaded environment. + +[[multi-strict]] +== Strict Message Ordering in a Multi-Threaded Environment + +The discussion in xref:amqp/template.adoc#scoped-operations[Scoped Operations] applies only when the operations are performed on the same thread. + +Consider the following situation: + +* `thread-1` sends a message to a queue and hands off work to `thread-2` +* `thread-2` sends a message to the same queue + +Because of the async nature of RabbitMQ and the use of cached channels; it is not certain that the same channel will be used and therefore the order in which the messages arrive in the queue is not guaranteed. +(In most cases they will arrive in order, but the probability of out-of-order delivery is not zero). +To solve this use case, you can use a bounded channel cache with size `1` (together with a `channelCheckoutTimeout`) to ensure the messages are always published on the same channel, and order will be guaranteed. +To do this, if you have other uses for the connection factory, such as consumers, you should either use a dedicated connection factory for the template, or configure the template to use the publisher connection factory embedded in the main connection factory (see xref:amqp/template.adoc#separate-connection[Using a Separate Connection]). + +This is best illustrated with a simple Spring Boot Application: + +[source, java] +---- +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + TaskExecutor exec() { + ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); + exec.setCorePoolSize(10); + return exec; + } + + @Bean + CachingConnectionFactory ccf() { + CachingConnectionFactory ccf = new CachingConnectionFactory("localhost"); + CachingConnectionFactory publisherCF = (CachingConnectionFactory) ccf.getPublisherConnectionFactory(); + publisherCF.setChannelCacheSize(1); + publisherCF.setChannelCheckoutTimeout(1000L); + return ccf; + } + + @RabbitListener(queues = "queue") + void listen(String in) { + log.info(in); + } + + @Bean + Queue queue() { + return new Queue("queue"); + } + + + @Bean + public ApplicationRunner runner(Service service, TaskExecutor exec) { + return args -> { + exec.execute(() -> service.mainService("test")); + }; + } + +} + +@Component +class Service { + + private static final Logger LOG = LoggerFactory.getLogger(Service.class); + + private final RabbitTemplate template; + + private final TaskExecutor exec; + + Service(RabbitTemplate template, TaskExecutor exec) { + template.setUsePublisherConnection(true); + this.template = template; + this.exec = exec; + } + + void mainService(String toSend) { + LOG.info("Publishing from main service"); + this.template.convertAndSend("queue", toSend); + this.exec.execute(() -> secondaryService(toSend.toUpperCase())); + } + + void secondaryService(String toSend) { + LOG.info("Publishing from secondary service"); + this.template.convertAndSend("queue", toSend); + } + +} +---- + +Even though the publishing is performed on two different threads, they will both use the same channel because the cache is capped at a single channel. + +Starting with version 2.3.7, the `ThreadChannelConnectionFactory` supports transferring a thread's channel(s) to another thread, using the `prepareContextSwitch` and `switchContext` methods. +The first method returns a context which is passed to the second thread which calls the second method. +A thread can have either a non-transactional channel or a transactional channel (or one of each) bound to it; you cannot transfer them individually, unless you use two connection factories. +An example follows: + +[source, java] +---- +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + TaskExecutor exec() { + ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); + exec.setCorePoolSize(10); + return exec; + } + + @Bean + ThreadChannelConnectionFactory tccf() { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + return new ThreadChannelConnectionFactory(rabbitConnectionFactory); + } + + @RabbitListener(queues = "queue") + void listen(String in) { + log.info(in); + } + + @Bean + Queue queue() { + return new Queue("queue"); + } + + + @Bean + public ApplicationRunner runner(Service service, TaskExecutor exec) { + return args -> { + exec.execute(() -> service.mainService("test")); + }; + } + +} + +@Component +class Service { + + private static final Logger LOG = LoggerFactory.getLogger(Service.class); + + private final RabbitTemplate template; + + private final TaskExecutor exec; + + private final ThreadChannelConnectionFactory connFactory; + + Service(RabbitTemplate template, TaskExecutor exec, + ThreadChannelConnectionFactory tccf) { + + this.template = template; + this.exec = exec; + this.connFactory = tccf; + } + + void mainService(String toSend) { + LOG.info("Publishing from main service"); + this.template.convertAndSend("queue", toSend); + Object context = this.connFactory.prepareSwitchContext(); + this.exec.execute(() -> secondaryService(toSend.toUpperCase(), context)); + } + + void secondaryService(String toSend, Object threadContext) { + LOG.info("Publishing from secondary service"); + this.connFactory.switchContext(threadContext); + this.template.convertAndSend("queue", toSend); + this.connFactory.closeThreadChannel(); + } + +} +---- + +IMPORTANT: Once the `prepareSwitchContext` is called, if the current thread performs any more operations, they will be performed on a new channel. +It is important to close the thread-bound channel when it is no longer needed. + +[[template-messaging]] +== Messaging Integration + +Starting with version 1.4, `RabbitMessagingTemplate` (built on top of `RabbitTemplate`) provides an integration with the Spring Framework messaging abstraction -- that is, +`org.springframework.messaging.Message`. +This lets you send and receive messages by using the `spring-messaging` `Message` abstraction. +This abstraction is used by other Spring projects, such as Spring Integration and Spring's STOMP support. +There are two message converters involved: one to convert between a spring-messaging `Message` and Spring AMQP's `Message` abstraction and one to convert between Spring AMQP's `Message` abstraction and the format required by the underlying RabbitMQ client library. +By default, the message payload is converted by the provided `RabbitTemplate` instance's message converter. +Alternatively, you can inject a custom `MessagingMessageConverter` with some other payload converter, as the following example shows: + +[source, java] +---- +MessagingMessageConverter amqpMessageConverter = new MessagingMessageConverter(); +amqpMessageConverter.setPayloadConverter(myPayloadConverter); +rabbitMessagingTemplate.setAmqpMessageConverter(amqpMessageConverter); +---- + +[[template-user-id]] +== Validated User Id + +Starting with version 1.6, the template now supports a `user-id-expression` (`userIdExpression` when using Java configuration). +If a message is sent, the user id property is set (if not already set) after evaluating this expression. +The root object for the evaluation is the message to be sent. + +The following examples show how to use the `user-id-expression` attribute: + +[source, xml] +---- + + + +---- + +The first example is a literal expression. +The second obtains the `username` property from a connection factory bean in the application context. + +[[separate-connection]] +== Using a Separate Connection + +Starting with version 2.0.2, you can set the `usePublisherConnection` property to `true` to use a different connection to that used by listener containers, when possible. +This is to avoid consumers being blocked when a producer is blocked for any reason. +The connection factories maintain a second internal connection factory for this purpose; by default it is the same type as the main factory, but can be set explicitly if you wish to use a different factory type for publishing. +If the rabbit template is running in a transaction started by the listener container, the container's channel is used, regardless of this setting. + +IMPORTANT: In general, you should not use a `RabbitAdmin` with a template that has this set to `true`. +Use the `RabbitAdmin` constructor that takes a connection factory. +If you use the other constructor that takes a template, ensure the template's property is `false`. +This is because, often, an admin is used to declare queues for listener containers. +Using a template that has the property set to `true` would mean that exclusive queues (such as `AnonymousQueue`) would be declared on a different connection to that used by listener containers. +In that case, the queues cannot be used by the containers. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc new file mode 100644 index 0000000000..796be4f4db --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc @@ -0,0 +1,157 @@ +[[transactions]] += Transactions + +The Spring Rabbit framework has support for automatic transaction management in the synchronous and asynchronous use cases with a number of different semantics that can be selected declaratively, as is familiar to existing users of Spring transactions. +This makes many if not most common messaging patterns easy to implement. + +There are two ways to signal the desired transaction semantics to the framework. +In both the `RabbitTemplate` and `SimpleMessageListenerContainer`, there is a flag `channelTransacted` which, if `true`, tells the framework to use a transactional channel and to end all operations (send or receive) with a commit or rollback (depending on the outcome), with an exception signaling a rollback. +Another signal is to provide an external transaction with one of Spring's `PlatformTransactionManager` implementations as a context for the ongoing operation. +If there is already a transaction in progress when the framework is sending or receiving a message, and the `channelTransacted` flag is `true`, the commit or rollback of the messaging transaction is deferred until the end of the current transaction. +If the `channelTransacted` flag is `false`, no transaction semantics apply to the messaging operation (it is auto-acked). + +The `channelTransacted` flag is a configuration time setting. +It is declared and processed once when the AMQP components are created, usually at application startup. +The external transaction is more dynamic in principle because the system responds to the current thread state at runtime. +However, in practice, it is often also a configuration setting, when the transactions are layered onto an application declaratively. + +For synchronous use cases with `RabbitTemplate`, the external transaction is provided by the caller, either declaratively or imperatively according to taste (the usual Spring transaction model). +The following example shows a declarative approach (usually preferred because it is non-invasive), where the template has been configured with `channelTransacted=true`: + +[source,java] +---- +@Transactional +public void doSomething() { + String incoming = rabbitTemplate.receiveAndConvert(); + // do some more database processing... + String outgoing = processInDatabaseAndExtractReply(incoming); + rabbitTemplate.convertAndSend(outgoing); +} +---- + +In the preceding example, a `String` payload is received, converted, and sent as a message body inside a method marked as `@Transactional`. +If the database processing fails with an exception, the incoming message is returned to the broker, and the outgoing message is not sent. +This applies to any operations with the `RabbitTemplate` inside a chain of transactional methods (unless, for instance, the `Channel` is directly manipulated to commit the transaction early). + +For asynchronous use cases with `SimpleMessageListenerContainer`, if an external transaction is needed, it has to be requested by the container when it sets up the listener. +To signal that an external transaction is required, the user provides an implementation of `PlatformTransactionManager` to the container when it is configured. +The following example shows how to do so: + +[source,java] +---- +@Configuration +public class ExampleExternalTransactionAmqpConfiguration { + + @Bean + public SimpleMessageListenerContainer messageListenerContainer() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(rabbitConnectionFactory()); + container.setTransactionManager(transactionManager()); + container.setChannelTransacted(true); + container.setQueueName("some.queue"); + container.setMessageListener(exampleListener()); + return container; + } + +} +---- + +In the preceding example, the transaction manager is added as a dependency injected from another bean definition (not shown), and the `channelTransacted` flag is also set to `true`. +The effect is that if the listener fails with an exception, the transaction is rolled back, and the message is also returned to the broker. +Significantly, if the transaction fails to commit (for example, because of +a database constraint error or connectivity problem), the AMQP transaction is also rolled back, and the message is returned to the broker. +This is sometimes known as a "`Best Efforts 1 Phase Commit`", and is a very powerful pattern for reliable messaging. +If the `channelTransacted` flag was set to `false` (the default) in the preceding example, the external transaction would still be provided for the listener, but all messaging operations would be auto-acked, so the effect is to commit the messaging operations even on a rollback of the business operation. + +[[conditional-rollback]] +== Conditional Rollback + +Prior to version 1.6.6, adding a rollback rule to a container's `transactionAttribute` when using an external transaction manager (such as JDBC) had no effect. +Exceptions always rolled back the transaction. + +Also, when using a {spring-framework-docs}/data-access/transaction/declarative.html[transaction advice] in the container's advice chain, conditional rollback was not very useful, because all listener exceptions are wrapped in a `ListenerExecutionFailedException`. + +The first problem has been corrected, and the rules are now applied properly. +Further, the `ListenerFailedRuleBasedTransactionAttribute` is now provided. +It is a subclass of `RuleBasedTransactionAttribute`, with the only difference being that it is aware of the `ListenerExecutionFailedException` and uses the cause of such exceptions for the rule. +This transaction attribute can be used directly in the container or through a transaction advice. + +The following example uses this rule: + +[source, java] +---- +@Bean +public AbstractMessageListenerContainer container() { + ... + container.setTransactionManager(transactionManager); + RuleBasedTransactionAttribute transactionAttribute = + new ListenerFailedRuleBasedTransactionAttribute(); + transactionAttribute.setRollbackRules(Collections.singletonList( + new NoRollbackRuleAttribute(DontRollBackException.class))); + container.setTransactionAttribute(transactionAttribute); + ... +} +---- + +[[transaction-rollback]] +== A note on Rollback of Received Messages + +AMQP transactions apply only to messages and acks sent to the broker. +Consequently, when there is a rollback of a Spring transaction and a message has been received, Spring AMQP has to not only rollback the transaction but also manually reject the message (sort of a nack, but that is not what the specification calls it). +The action taken on message rejection is independent of transactions and depends on the `defaultRequeueRejected` property (default: `true`). +For more information about rejecting failed messages, see xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case]. + +For more information about RabbitMQ transactions and their limitations, see https://www.rabbitmq.com/semantics.html[RabbitMQ Broker Semantics]. + +NOTE: Prior to RabbitMQ 2.7.0, such messages (and any that are unacked when a channel is closed or aborts) went to the back of the queue on a Rabbit broker. +Since 2.7.0, rejected messages go to the front of the queue, in a similar manner to JMS rolled back messages. + +NOTE: Previously, message requeue on transaction rollback was inconsistent between local transactions and when a `TransactionManager` was provided. +In the former case, the normal requeue logic (`AmqpRejectAndDontRequeueException` or `defaultRequeueRejected=false`) applied (see xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case]). +With a transaction manager, the message was unconditionally requeued on rollback. +Starting with version 2.0, the behavior is consistent and the normal requeue logic is applied in both cases. +To revert to the previous behavior, you can set the container's `alwaysRequeueWithTxManagerRollback` property to `true`. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + +[[using-rabbittransactionmanager]] +== Using `RabbitTransactionManager` + +The javadoc:org.springframework.amqp.rabbit.transaction.RabbitTransactionManager[] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. +This transaction manager is an implementation of the javadoc:org.springframework.transaction.PlatformTransactionManager[] interface and should be used with a single Rabbit `ConnectionFactory`. + +IMPORTANT: This strategy is not able to provide XA transactions -- for example, in order to share transactions between messaging and database access. + +Application code is required to retrieve the transactional Rabbit resources through `ConnectionFactoryUtils.getTransactionalResourceHolder(ConnectionFactory, boolean)` instead of a standard `Connection.createChannel()` call with subsequent channel creation. +When using Spring AMQP's javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[], it will autodetect a thread-bound Channel and automatically participate in its transaction. + +With Java Configuration, you can setup a new RabbitTransactionManager by using the following bean: + +[source,java] +---- +@Bean +public RabbitTransactionManager rabbitTransactionManager() { + return new RabbitTransactionManager(connectionFactory); +} +---- + +If you prefer XML configuration, you can declare the following bean in your XML Application Context file: + +[source,xml] +---- + + + +---- + +[[tx-sync]] +== Transaction Synchronization + +Synchronizing a RabbitMQ transaction with some other (e.g. DBMS) transaction provides "Best Effort One Phase Commit" semantics. +It is possible that the RabbitMQ transaction fails to commit during the after completion phase of transaction synchronization. +This is logged by the `spring-tx` infrastructure as an error, but no exception is thrown to the calling code. +Starting with version 2.3.10, you can call `ConnectionUtils.checkAfterCompletion()` after the transaction has committed on the same thread that processed the transaction. +It will simply return if no exception occurred; otherwise it will throw an `AfterCompletionFailedException` which will have a property representing the synchronization status of the completion. + +Enable this feature by calling `ConnectionFactoryUtils.enableAfterCompletionFailureCapture(true)`; this is a global flag and applies to all threads. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/change-history.adoc b/src/reference/antora/modules/ROOT/pages/appendix/change-history.adoc new file mode 100644 index 0000000000..85b97003ba --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/change-history.adoc @@ -0,0 +1,5 @@ +[[change-history]] += Change History +:page-section-summary-toc: 1 + +This section describes changes that have been made as versions have changed. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/current-release.adoc b/src/reference/antora/modules/ROOT/pages/appendix/current-release.adoc new file mode 100644 index 0000000000..e76e79e541 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/current-release.adoc @@ -0,0 +1,6 @@ +[[current-release]] += Current Release +:page-section-summary-toc: 1 + +See xref:whats-new.adoc[What's New]. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/micrometer.adoc b/src/reference/antora/modules/ROOT/pages/appendix/micrometer.adoc new file mode 100644 index 0000000000..0ec7789c76 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/micrometer.adoc @@ -0,0 +1,8 @@ +[[observation-gen]] += Micrometer Observation Documentation + +This section describes the Micrometer integration. + +include::partial$metrics.adoc[leveloffset=-1] +include::partial$spans.adoc[leveloffset=-1] +include::partial$conventions.adoc[leveloffset=-1] diff --git a/src/reference/antora/modules/ROOT/pages/appendix/native.adoc b/src/reference/antora/modules/ROOT/pages/appendix/native.adoc new file mode 100644 index 0000000000..8234dc91e6 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/native.adoc @@ -0,0 +1,7 @@ +[[native-images]] += Native Images +:page-section-summary-toc: 1 + +{spring-framework-docs}/core/aot.html[Spring AOT] native hints are provided to assist in developing native images for Spring applications that use Spring AMQP. + +Some examples can be seen in the https://github.com/spring-projects/spring-aot-smoke-tests/tree/main/integration[`spring-aot-smoke-tests` GitHub repository]. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new.adoc new file mode 100644 index 0000000000..d9f5c27873 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new.adoc @@ -0,0 +1,4 @@ +[[previous-whats-new]] += Previous Releases +:page-section-summary-toc: 1 + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc new file mode 100644 index 0000000000..da18f901ca --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc @@ -0,0 +1,106 @@ +[[changes-in-1-3-since-1-2]] += Changes in 1.3 Since 1.2 + +[[listener-concurrency]] +== Listener Concurrency + +The listener container now supports dynamic scaling of the number of consumers based on workload, or you can programmatically change the concurrency without stopping the container. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. + +[[listener-queues]] +== Listener Queues + +The listener container now permits the queues on which it listens to be modified at runtime. +Also, the container now starts if at least one of its configured queues is available for use. +See xref:amqp/listener-queues.adoc#listener-queues[Listener Container Queues] + +This listener container now redeclares any auto-delete queues during startup. +See xref:amqp/receiving-messages/async-consumer.adoc#lc-auto-delete[`auto-delete` Queues]. + +[[consumer-priority]] +== Consumer Priority + +The listener container now supports consumer arguments, letting the `x-priority` argument be set. +See xref:amqp/receiving-messages/async-consumer.adoc#consumer-priority[Consumer Priority]. + +[[exclusive-consumer]] +== Exclusive Consumer + +You can now configure `SimpleMessageListenerContainer` with a single `exclusive` consumer, preventing other consumers from listening to the queue. +See xref:amqp/exclusive-consumer.adoc[Exclusive Consumer]. + +[[rabbit-admin]] +== Rabbit Admin + +You can now have the broker generate the queue name, regardless of `durable`, `autoDelete`, and `exclusive` settings. +See xref:amqp/broker-configuration.adoc[Configuring the Broker]. + +[[direct-exchange-binding]] +== Direct Exchange Binding + +Previously, omitting the `key` attribute from a `binding` element of a `direct-exchange` configuration caused the queue or exchange to be bound with an empty string as the routing key. +Now it is bound with the the name of the provided `Queue` or `Exchange`. +If you wish to bind with an empty string routing key, you need to specify `key=""`. + +[[amqptemplate-changes]] +== `AmqpTemplate` Changes + +The `AmqpTemplate` now provides several synchronous `receiveAndReply` methods. +These are implemented by the `RabbitTemplate`. +For more information see xref:amqp/receiving-messages.adoc[Receiving Messages]. + +The `RabbitTemplate` now supports configuring a `RetryTemplate` to attempt retries (with optional back-off policy) for when the broker is not available. +For more information see xref:amqp/template.adoc#template-retry[Adding Retry Capabilities]. + +[[caching-connection-factory]] +== Caching Connection Factory + +You can now configure the caching connection factory to cache `Connection` instances and their `Channel` instances instead of using a single connection and caching only `Channel` instances. +See xref:amqp/connections.adoc[Connection and Resource Management]. + +[[binding-arguments]] +== Binding Arguments + +The `` of the `` now supports parsing of the `` sub-element. +You can now configure the `` of the `` with a `key/value` attribute pair (to match on a single header) or with a `` sub-element (allowing matching on multiple headers). +These options are mutually exclusive. +See xref:amqp/broker-configuration.adoc#headers-exchange[Headers Exchange]. + +== Routing Connection Factory + +A new `SimpleRoutingConnectionFactory` has been introduced. +It allows configuration of `ConnectionFactories` mapping, to determine the target `ConnectionFactory` to use at runtime. +See xref:amqp/connections.adoc#routing-connection-factory[Routing Connection Factory]. + +[[messagebuilder-and-messagepropertiesbuilder]] +== `MessageBuilder` and `MessagePropertiesBuilder` + +"`Fluent APIs`" for building messages or message properties are now provided. +See xref:amqp/sending-messages.adoc#message-builder[Message Builder API]. + +[[retryinterceptorbuilder-change]] +== `RetryInterceptorBuilder` Change + +A "`Fluent API`" for building listener container retry interceptors is now provided. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#retry[Failures in Synchronous Operations and Options for Retry]. + +[[republishmessagerecoverer-added]] +== `RepublishMessageRecoverer` Added + +This new `MessageRecoverer` is provided to allow publishing a failed message to another queue (including stack trace information in the header) when retries are exhausted. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case]. + +[[default-error-handler-since-1-3-2]] +== Default Error Handler (Since 1.3.2) + +A default `ConditionalRejectingErrorHandler` has been added to the listener container. +This error handler detects fatal message conversion problems and instructs the container to reject the message to prevent the broker from continually redelivering the unconvertible message. +See xref:amqp/exception-handling.adoc[Exception Handling]. + +[[listener-container-missingqueuesfatal-property-since-1-3-5]] +== Listener Container `missingQueuesFatal` Property (Since 1.3.5) + +The `SimpleMessageListenerContainer` now has a property called `missingQueuesFatal` (default: `true`). +Previously, missing queues were always fatal. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc new file mode 100644 index 0000000000..fef889d846 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc @@ -0,0 +1,120 @@ +[[changes-in-1-4-since-1-3]] += Changes in 1.4 Since 1.3 + +[[rabbitlistener-annotation]] +== `@RabbitListener` Annotation + +POJO listeners can be annotated with `@RabbitListener`, enabled by `@EnableRabbit` or ``. +Spring Framework 4.1 is required for this feature. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +[[rabbitmessagingtemplate-added]] +== `RabbitMessagingTemplate` Added + +A new `RabbitMessagingTemplate` lets you interact with RabbitMQ by using `spring-messaging` `Message` instances. +Internally, it uses the `RabbitTemplate`, which you can configure as normal. +Spring Framework 4.1 is required for this feature. +See xref:amqp/template.adoc#template-messaging[Messaging Integration] for more information. + +[[listener-container-missingqueuesfatal-attribute]] +== Listener Container `missingQueuesFatal` Attribute + +1.3.5 introduced the `missingQueuesFatal` property on the `SimpleMessageListenerContainer`. +This is now available on the listener container namespace element. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + +[[rabbittemplate-confirmcallback-interface]] +== RabbitTemplate `ConfirmCallback` Interface + +The `confirm` method on this interface has an additional parameter called `cause`. +When available, this parameter contains the reason for a negative acknowledgement (nack). +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. + +[[rabbitconnectionfactorybean-added]] +== `RabbitConnectionFactoryBean` Added + +`RabbitConnectionFactoryBean` creates the underlying RabbitMQ `ConnectionFactory` used by the `CachingConnectionFactory`. +This enables configuration of SSL options using Spring's dependency injection. +See xref:amqp/connections.adoc#connection-factory[Configuring the Underlying Client Connection Factory]. + +[[using-cachingconnectionfactory]] +== Using `CachingConnectionFactory` + +The `CachingConnectionFactory` now lets the `connectionTimeout` be set as a property or as an attribute in the namespace. +It sets the property on the underlying RabbitMQ `ConnectionFactory`. +See xref:amqp/connections.adoc#connection-factory[Configuring the Underlying Client Connection Factory]. + +[[log-appender]] +== Log Appender + +The Logback `org.springframework.amqp.rabbit.logback.AmqpAppender` has been introduced. +It provides options similar to `org.springframework.amqp.rabbit.log4j.AmqpAppender`. +For more information, see the JavaDoc of these classes. + +The Log4j `AmqpAppender` now supports the `deliveryMode` property (`PERSISTENT` or `NON_PERSISTENT`, default: `PERSISTENT`). +Previously, all log4j messages were `PERSISTENT`. + +The appender also supports modification of the `Message` before sending -- allowing, for example, the addition of custom headers. +Subclasses should override the `postProcessMessageBeforeSend()`. + +[[listener-queues]] +== Listener Queues + +The listener container now, by default, redeclares any missing queues during startup. +A new `auto-declare` attribute has been added to the `` to prevent these re-declarations. +See xref:amqp/receiving-messages/async-consumer.adoc#lc-auto-delete[`auto-delete` Queues]. + +[[rabbittemplate:-mandatory-and-connectionfactoryselector-expressions]] +== `RabbitTemplate`: `mandatory` and `connectionFactorySelector` Expressions + +The `mandatoryExpression`, `sendConnectionFactorySelectorExpression`, and `receiveConnectionFactorySelectorExpression` SpEL Expression`s properties have been added to `RabbitTemplate`. +The `mandatoryExpression` is used to evaluate a `mandatory` boolean value against each request message when a `ReturnCallback` is in use. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. +The `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` are used when an `AbstractRoutingConnectionFactory` is provided, to determine the `lookupKey` for the target `ConnectionFactory` at runtime on each AMQP protocol interaction operation. +See xref:amqp/connections.adoc#routing-connection-factory[Routing Connection Factory]. + +[[listeners-and-the-routing-connection-factory]] +== Listeners and the Routing Connection Factory + +You can configure a `SimpleMessageListenerContainer` with a routing connection factory to enable connection selection based on the queue names. +See xref:amqp/connections.adoc#routing-connection-factory[Routing Connection Factory]. + +[[rabbittemplate:-recoverycallback-option]] +== `RabbitTemplate`: `RecoveryCallback` Option + +The `recoveryCallback` property has been added for use in the `retryTemplate.execute()`. +See xref:amqp/template.adoc#template-retry[Adding Retry Capabilities]. + +[[messageconversionexception-change]] +== `MessageConversionException` Change + +This exception is now a subclass of `AmqpException`. +Consider the following code: + +[source,java] +---- +try { + template.convertAndSend("thing1", "thing2", "cat"); +} +catch (AmqpException e) { + ... +} +catch (MessageConversionException e) { + ... +} +---- + +The second catch block is no longer reachable and needs to be moved above the catch-all `AmqpException` catch block. + +[[rabbitmq-3-4-compatibility]] +== RabbitMQ 3.4 Compatibility + +Spring AMQP is now compatible with the RabbitMQ 3.4, including direct reply-to. +See xref:introduction/quick-tour.adoc#compatibility[Compatibility] and xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] for more information. + +[[contenttypedelegatingmessageconverter-added]] +== `ContentTypeDelegatingMessageConverter` Added + +The `ContentTypeDelegatingMessageConverter` has been introduced to select the `MessageConverter` to use, based on the `contentType` property in the `MessageProperties`. +See xref:amqp/message-converters.adoc[Message Converters] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc new file mode 100644 index 0000000000..4b5621cb0e --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc @@ -0,0 +1,197 @@ +[[changes-in-1-5-since-1-4]] += Changes in 1.5 Since 1.4 + +[[spring-erlang-is-no-longer-supported]] +== `spring-erlang` Is No Longer Supported + +The `spring-erlang` jar is no longer included in the distribution. +Use xref:amqp/management-rest-api.adoc#management-rest-api[RabbitMQ REST API] instead. + +[[cachingconnectionfactory-changes]] +== `CachingConnectionFactory` Changes + +[[empty-addresses-property-in-cachingconnectionfactory]] +=== Empty Addresses Property in `CachingConnectionFactory` + +Previously, if the connection factory was configured with a host and port but an empty String was also supplied for +`addresses`, the host and port were ignored. +Now, an empty `addresses` String is treated the same as a `null`, and the host and port are used. + +[[uri-constructor]] +=== URI Constructor + +The `CachingConnectionFactory` has an additional constructor, with a `URI` parameter, to configure the broker connection. + +[[connection-reset]] +=== Connection Reset + +A new method called `resetConnection()` has been added to let users reset the connection (or connections). +You might use this, for example, to reconnect to the primary broker after failing over to the secondary broker. +This *does* impact in-process operations. +The existing `destroy()` method does exactly the same, but the new method has a less daunting name. + +[[properties-to-control-container-queue-declaration-behavior]] +== Properties to Control Container Queue Declaration Behavior + +When the listener container consumers start, they attempt to passively declare the queues to ensure they are available +on the broker. +Previously, if these declarations failed (for example, because the queues didn't exist) or when an HA queue was being +moved, the retry logic was fixed at three retry attempts at five-second intervals. +If the queues still do not exist, the behavior is controlled by the `missingQueuesFatal` property (default: `true`). +Also, for containers configured to listen from multiple queues, if only a subset of queues are available, the consumer +retried the missing queues on a fixed interval of 60 seconds. + +The `declarationRetries`, `failedDeclarationRetryInterval`, and `retryDeclarationInterval` properties are now configurable. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[class-package-change]] +== Class Package Change + +The `RabbitGatewaySupport` class has been moved from `o.s.amqp.rabbit.core.support` to `o.s.amqp.rabbit.core`. + +[[defaultmessagepropertiesconverter-changes]] +== `DefaultMessagePropertiesConverter` Changes + +You can now configure the `DefaultMessagePropertiesConverter` to +determine the maximum length of a `LongString` that is converted +to a `String` rather than to a `DataInputStream`. +The converter has an alternative constructor that takes the value as a limit. +Previously, this limit was hard-coded at `1024` bytes. +(Also available in 1.4.4). + +[[rabbitlistener-improvements]] +== `@RabbitListener` Improvements + +[[queuebinding-for-rabbitlistener]] +=== `@QueueBinding` for `@RabbitListener` + +The `bindings` attribute has been added to the `@RabbitListener` annotation as mutually exclusive with the `queues` +attribute to allow the specification of the `queue`, its `exchange`, and `binding` for declaration by a `RabbitAdmin` on +the Broker. + +[[spel-in-sendto]] +=== SpEL in `@SendTo` + +The default reply address (`@SendTo`) for a `@RabbitListener` can now be a SpEL expression. + +[[multiple-queue-names-through-properties]] +=== Multiple Queue Names through Properties + +You can now use a combination of SpEL and property placeholders to specify multiple queues for a listener. + +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +[[automatic-exchange-queue-and-binding-declaration]] +== Automatic Exchange, Queue, and Binding Declaration + +You can now declare beans that define a collection of these entities, and the `RabbitAdmin` adds the +contents to the list of entities that it declares when a connection is established. +See xref:amqp/broker-configuration.adoc#collection-declaration[Declaring Collections of Exchanges, Queues, and Bindings] for more information. + +[[rabbittemplate-changes]] +== `RabbitTemplate` Changes + +[[reply-address-added]] +=== `reply-address` Added + +The `reply-address` attribute has been added to the `` component as an alternative `reply-queue`. +See xref:amqp/request-reply.adoc[Request/Reply Messaging] for more information. +(Also available in 1.4.4 as a setter on the `RabbitTemplate`). + +[[blocking-receive-methods]] +=== Blocking `receive` Methods + +The `RabbitTemplate` now supports blocking in `receive` and `convertAndReceive` methods. +See xref:amqp/receiving-messages/polling-consumer.adoc[Polling Consumer] for more information. + +[[mandatory-with-sendandreceive-methods]] +=== Mandatory with `sendAndReceive` Methods + +When the `mandatory` flag is set when using the `sendAndReceive` and `convertSendAndReceive` methods, the calling thread +throws an `AmqpMessageReturnedException` if the request message cannot be delivered. +See xref:amqp/request-reply.adoc#reply-timeout[Reply Timeout] for more information. + +[[improper-reply-listener-configuration]] +=== Improper Reply Listener Configuration + +The framework tries to verify proper configuration of a reply listener container when using a named reply queue. + +See xref:amqp/request-reply.adoc#reply-listener[Reply Listener Container] for more information. + +[[rabbitmanagementtemplate-added]] +== `RabbitManagementTemplate` Added + +The `RabbitManagementTemplate` has been introduced to monitor and configure the RabbitMQ Broker by using the REST API provided by its https://www.rabbitmq.com/management.html[management plugin]. +See xref:amqp/management-rest-api.adoc#management-rest-api[RabbitMQ REST API] for more information. + +[[listener-container-bean-names-xml]] +== Listener Container Bean Names (XML) + +[IMPORTANT] +==== +The `id` attribute on the `` element has been removed. +Starting with this release, the `id` on the `` child element is used alone to name the listener container bean created for each listener element. + +Normal Spring bean name overrides are applied. +If a later `` is parsed with the same `id` as an existing bean, the new definition overrides the existing one. +Previously, bean names were composed from the `id` attributes of the `` and `` elements. + +When migrating to this release, if you have `id` attributes on your `` elements, remove them and set the `id` on the child `` element instead. +==== + +However, to support starting and stopping containers as a group, a new `group` attribute has been added. +When this attribute is defined, the containers created by this element are added to a bean with this name, of type `Collection`. +You can iterate over this group to start and stop containers. + +[[class-level-rabbitlistener]] +== Class-Level `@RabbitListener` + +The `@RabbitListener` annotation can now be applied at the class level. +Together with the new `@RabbitHandler` method annotation, this lets you select the handler method based on payload type. +See xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[Multi-method Listeners] for more information. + +[[simplemessagelistenercontainer:-backoff-support]] +== `SimpleMessageListenerContainer`: BackOff Support + +The `SimpleMessageListenerContainer` can now be supplied with a `BackOff` instance for `consumer` startup recovery. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[channel-close-logging]] +== Channel Close Logging + +A mechanism to control the log levels of channel closure has been introduced. +See xref:amqp/connections.adoc#channel-close-logging[Logging Channel Close Events]. + +[[application-events]] +== Application Events + +The `SimpleMessageListenerContainer` now emits application events when consumers fail. +See xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events] for more information. + +[[consumer-tag-configuration]] +== Consumer Tag Configuration + +Previously, the consumer tags for asynchronous consumers were generated by the broker. +With this release, it is now possible to supply a naming strategy to the listener container. +See xref:amqp/receiving-messages/consumerTags.adoc[Consumer Tags]. + +[[using-messagelisteneradapter]] +== Using `MessageListenerAdapter` + +The `MessageListenerAdapter` now supports a map of queue names (or consumer tags) to method names, to determine +which delegate method to call based on the queue from which the message was received. + +[[localizedqueueconnectionfactory-added]] +== `LocalizedQueueConnectionFactory` Added + +`LocalizedQueueConnectionFactory` is a new connection factory that connects to the node in a cluster where a mirrored queue actually resides. + +See xref:amqp/connections.adoc#queue-affinity[Queue Affinity and the `LocalizedQueueConnectionFactory`]. + +[[anonymous-queue-naming]] +== Anonymous Queue Naming + +Starting with version 1.5.3, you can now control how `AnonymousQueue` names are generated. +See xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`] for more information. + + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc new file mode 100644 index 0000000000..2e96f8fea2 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc @@ -0,0 +1,262 @@ +[[changes-in-1-6-since-1-5]] += Changes in 1.6 Since 1.5 + +[[testing-support]] +== Testing Support + +A new testing support library is now provided. +See xref:testing.adoc[Testing Support] for more information. + +[[builder]] +== Builder + +Builders that provide a fluent API for configuring `Queue` and `Exchange` objects are now available. +See xref:amqp/broker-configuration.adoc#builder-api[Builder API for Queues and Exchanges] for more information. + +[[namespace-changes]] +== Namespace Changes + +[[connection-factory]] +=== Connection Factory + +You can now add a `thread-factory` to a connection factory bean declaration -- for example, to name the threads +created by the `amqp-client` library. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +When you use `CacheMode.CONNECTION`, you can now limit the total number of connections allowed. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +[[queue-definitions]] +=== Queue Definitions + +You can now provide a naming strategy for anonymous queues. +See xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`] for more information. + +[[listener-container-changes]] +== Listener Container Changes + +[[idle-message-listener-detection]] +=== Idle Message Listener Detection + +You can now configure listener containers to publish `ApplicationEvent` instances when idle. +See xref:amqp/receiving-messages/idle-containers.adoc[Detecting Idle Asynchronous Consumers] for more information. + +[[mismatched-queue-detection]] +=== Mismatched Queue Detection + +By default, when a listener container starts, if queues with mismatched properties or arguments are detected, +the container logs the exception but continues to listen. +The container now has a property called `mismatchedQueuesFatal`, which prevents the container (and context) from +starting if the problem is detected during startup. +It also stops the container if the problem is detected later, such as after recovering from a connection failure. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[listener-container-logging]] +=== Listener Container Logging + +Now, listener container provides its `beanName` to the internal `SimpleAsyncTaskExecutor` as a `threadNamePrefix`. +It is useful for logs analysis. + +[[default-error-handler]] +=== Default Error Handler + +The default error handler (`ConditionalRejectingErrorHandler`) now considers irrecoverable `@RabbitListener` +exceptions as fatal. +See xref:amqp/exception-handling.adoc[Exception Handling] for more information. + + +[[autodeclare-and-rabbitadmin-instances]] +== `AutoDeclare` and `RabbitAdmin` Instances + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] (`autoDeclare`) for some changes to the semantics of that option with respect to the use +of `RabbitAdmin` instances in the application context. + +[[amqptemplate:-receive-with-timeout]] +== `AmqpTemplate`: Receive with Timeout + +A number of new `receive()` methods with `timeout` have been introduced for the `AmqpTemplate` +and its `RabbitTemplate` implementation. +See xref:amqp/receiving-messages/polling-consumer.adoc[Polling Consumer] for more information. + +[[using-asyncrabbittemplate]] +== Using `AsyncRabbitTemplate` + +A new `AsyncRabbitTemplate` has been introduced. +This template provides a number of send and receive methods, where the return value is a `ListenableFuture`, which can +be used later to obtain the result either synchronously or asynchronously. +See xref:amqp/request-reply.adoc#async-template[Async Rabbit Template] for more information. + +[[rabbittemplate-changes]] +== `RabbitTemplate` Changes + +1.4.1 introduced the ability to use https://www.rabbitmq.com/direct-reply-to.html[direct reply-to] when the broker supports it. +It is more efficient than using a temporary queue for each reply. +This version lets you override this default behavior and use a temporary queue by setting the `useTemporaryReplyQueues` property to `true`. +See xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] for more information. + +The `RabbitTemplate` now supports a `user-id-expression` (`userIdExpression` when using Java configuration). +See https://www.rabbitmq.com/validated-user-id.html[Validated User-ID RabbitMQ documentation] and xref:amqp/template.adoc#template-user-id[Validated User Id] for more information. + +[[message-properties]] +== Message Properties + +[[using-correlationid]] +=== Using `CorrelationId` + +The `correlationId` message property can now be a `String`. +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +[[long-string-headers]] +=== Long String Headers + +Previously, the `DefaultMessagePropertiesConverter` "`converted`" headers longer than the long string limit (default 1024) +to a `DataInputStream` (actually, it referenced the `LongString` instance's `DataInputStream`). +On output, this header was not converted (except to a String -- for example, `java.io.DataInputStream@1d057a39` by calling +`toString()` on the stream). + +With this release, long `LongString` instances are now left as `LongString` instances by default. +You can access the contents by using the `getBytes[]`, `toString()`, or `getStream()` methods. +A large incoming `LongString` is now correctly "`converted`" on output too. + +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +[[inbound-delivery-mode]] +=== Inbound Delivery Mode + +The `deliveryMode` property is no longer mapped to the `MessageProperties.deliveryMode`. +This change avoids unintended propagation if the the same `MessageProperties` object is used to send an outbound message. +Instead, the inbound `deliveryMode` header is mapped to `MessageProperties.receivedDeliveryMode`. + +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +When using annotated endpoints, the header is provided in the header named `AmqpHeaders.RECEIVED_DELIVERY_MODE`. + +See xref:amqp/receiving-messages/async-annotation-driven/enable-signature.adoc[Annotated Endpoint Method Signature] for more information. + +[[inbound-user-id]] +=== Inbound User ID + +The `user_id` property is no longer mapped to the `MessageProperties.userId`. +This change avoids unintended propagation if the the same `MessageProperties` object is used to send an outbound message. +Instead, the inbound `userId` header is mapped to `MessageProperties.receivedUserId`. + +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +When you use annotated endpoints, the header is provided in the header named `AmqpHeaders.RECEIVED_USER_ID`. + +See xref:amqp/receiving-messages/async-annotation-driven/enable-signature.adoc[Annotated Endpoint Method Signature] for more information. + +[[rabbitadmin-changes]] +== `RabbitAdmin` Changes + +[[declaration-failures]] +=== Declaration Failures + +Previously, the `ignoreDeclarationFailures` flag took effect only for `IOException` on the channel (such as mis-matched +arguments). +It now takes effect for any exception (such as `TimeoutException`). +In addition, a `DeclarationExceptionEvent` is now published whenever a declaration fails. +The `RabbitAdmin` last declaration event is also available as a property `lastDeclarationExceptionEvent`. +See xref:amqp/broker-configuration.adoc[Configuring the Broker] for more information. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +[[multiple-containers-for-each-bean]] +=== Multiple Containers for Each Bean + +When you use Java 8 or later, you can now add multiple `@RabbitListener` annotations to `@Bean` classes or +their methods. +When using Java 7 or earlier, you can use the `@RabbitListeners` container annotation to provide the same +functionality. +See xref:amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc[`@Repeatable` `@RabbitListener`] for more information. + +[[sendto-spel-expressions]] +=== `@SendTo` SpEL Expressions + +`@SendTo` for routing replies with no `replyTo` property can now be SpEL expressions evaluated against the +request/reply. +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for more information. + +[[queuebinding-improvements]] +=== `@QueueBinding` Improvements + +You can now specify arguments for queues, exchanges, and bindings in `@QueueBinding` annotations. +Header exchanges are now supported by `@QueueBinding`. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +[[delayed-message-exchange]] +== Delayed Message Exchange + +Spring AMQP now has first class support for the RabbitMQ Delayed Message Exchange plugin. +See xref:amqp/delayed-message-exchange.adoc[Delayed Message Exchange] for more information. + +[[exchange-internal-flag]] +== Exchange Internal Flag + +Any `Exchange` definitions can now be marked as `internal`, and `RabbitAdmin` passes the value to the broker when +declaring the exchange. +See xref:amqp/broker-configuration.adoc[Configuring the Broker] for more information. + +[[cachingconnectionfactory-changes]] +== `CachingConnectionFactory` Changes + +[[cachingconnectionfactory-cache-statistics]] +=== `CachingConnectionFactory` Cache Statistics + +The `CachingConnectionFactory` now provides cache properties at runtime and over JMX. +See xref:amqp/connections.adoc#runtime-cache-properties[Runtime Cache Properties] for more information. + +[[accessing-the-underlying-rabbitmq-connection-factory]] +=== Accessing the Underlying RabbitMQ Connection Factory + +A new getter has been added to provide access to the underlying factory. +You can use this getter, for example, to add custom connection properties. +See xref:amqp/custom-client-props.adoc[Adding Custom Client Connection Properties] for more information. + +[[channel-cache]] +=== Channel Cache + +The default channel cache size has been increased from 1 to 25. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +In addition, the `SimpleMessageListenerContainer` no longer adjusts the cache size to be at least as large as the number +of `concurrentConsumers` -- this was superfluous, since the container consumer channels are never cached. + +[[using-rabbitconnectionfactorybean]] +== Using `RabbitConnectionFactoryBean` + +The factory bean now exposes a property to add client connection properties to connections made by the resulting +factory. + +[[java-deserialization]] +== Java Deserialization + +You can now configure a "`allowed list`" of allowable classes when you use Java deserialization. +You should consider creating an allowed list if you accept messages with serialized java objects from +untrusted sources. +See amqp/message-converters.adoc#java-deserialization[Java Deserialization] for more information. + +[[json-messageconverter]] +== JSON `MessageConverter` + +Improvements to the JSON message converter now allow the consumption of messages that do not have type information +in message headers. +See xref:amqp/receiving-messages/async-annotation-driven/conversion.adoc[Message Conversion for Annotated Methods] and xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. + +[[logging-appenders]] +== Logging Appenders + +[[log4j-2]] +=== Log4j 2 + +A log4j 2 appender has been added, and the appenders can now be configured with an `addresses` property to connect +to a broker cluster. + +[[client-connection-properties]] +=== Client Connection Properties + +You can now add custom client connection properties to RabbitMQ connections. + +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc new file mode 100644 index 0000000000..054dd56e42 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc @@ -0,0 +1,76 @@ +[[changes-in-1-7-since-1-6]] += Changes in 1.7 Since 1.6 + +[[amqp-client-library]] +== AMQP Client library + +Spring AMQP now uses the new 4.0.x version of the `amqp-client` library provided by the RabbitMQ team. +This client has auto-recovery configured by default. +See xref:amqp/connections.adoc#auto-recovery[RabbitMQ Automatic Connection/Topology recovery]. + +NOTE: The 4.0.x client enables automatic recovery by default. +While compatible with this feature, Spring AMQP has its own recovery mechanisms, and the client recovery feature generally is not needed. +We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. +Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. +RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. + + +[[log4j-2-upgrade]] +== Log4j 2 upgrade +The minimum Log4j 2 version (for the `AmqpAppender`) is now `2.7`. +The framework is no longer compatible with previous versions. +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for more information. + +[[logback-appender]] +== Logback Appender + +This appender no longer captures caller data (method, line number) by default. +You can re-enable it by setting the `includeCallerData` configuration option. +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for information about the available log appenders. + +[[spring-retry-upgrade]] +== Spring Retry Upgrade + +The minimum Spring Retry version is now `1.2`. +The framework is no longer compatible with previous versions. + +[[shutdown-behavior]] +=== Shutdown Behavior + +You can now set `forceCloseChannel` to `true` so that, if the container threads do not respond to a shutdown within `shutdownTimeout`, the channels are forced closed, +causing any unacked messages to be re-queued. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[fasterxml-jackson-upgrade]] +== FasterXML Jackson upgrade + +The minimum Jackson version is now `2.8`. +The framework is no longer compatible with previous versions. + +[[junit-rules]] +== JUnit `@Rules` + +Rules that have previously been used internally by the framework have now been made available in a separate jar called `spring-rabbit-junit`. +See xref:testing.adoc#junit-rules[JUnit4 `@Rules`] for more information. + +[[container-conditional-rollback]] +== Container Conditional Rollback + +When you use an external transaction manager (such as JDBC), rule-based rollback is now supported when you provide the container with a transaction attribute. +It is also now more flexible when you use a transaction advice. + +[[connection-naming-strategy]] +== Connection Naming Strategy + +A new `ConnectionNameStrategy` is now provided to populate the application-specific identification of the target RabbitMQ connection from the `AbstractConnectionFactory`. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +[[listener-container-changes]] +== Listener Container Changes + +[[transaction-rollback-behavior]] +=== Transaction Rollback Behavior + +You can now configure message re-queue on transaction rollback to be consistent, regardless of whether or not a transaction manager is configured. +See xref:amqp/transactions.adoc#transaction-rollback[A note on Rollback of Received Messages] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc new file mode 100644 index 0000000000..92fd10598b --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc @@ -0,0 +1,203 @@ +[[changes-in-2-0-since-1-7]] += Changes in 2.0 Since 1.7 + +[[using-cachingconnectionfactory]] +== Using `CachingConnectionFactory` + +Starting with version 2.0.2, you can configure the `RabbitTemplate` to use a different connection to that used by listener containers. +This change avoids deadlocked consumers when producers are blocked for any reason. +See xref:amqp/template.adoc#separate-connection[Using a Separate Connection] for more information. + +[[amqp-client-library]] +== AMQP Client library + +Spring AMQP now uses the new 5.0.x version of the `amqp-client` library provided by the RabbitMQ team. +This client has auto recovery configured by default. +See xref:amqp/connections.adoc#auto-recovery[RabbitMQ Automatic Connection/Topology recovery]. + +NOTE: As of version 4.0, the client enables automatic recovery by default. +While compatible with this feature, Spring AMQP has its own recovery mechanisms, and the client recovery feature generally is not needed. +We recommend that you disable `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. +Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. +RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. + +[[general-changes]] +== General Changes + +The `ExchangeBuilder` now builds durable exchanges by default. +The `@Exchange` annotation used within a `@QeueueBinding` also declares durable exchanges by default. +The `@Queue` annotation used within a `@RabbitListener` by default declares durable queues if named and non-durable if anonymous. +See xref:amqp/broker-configuration.adoc#builder-api[Builder API for Queues and Exchanges] and xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +[[deleted-classes]] +== Deleted Classes + +`UniquelyNameQueue` is no longer provided. +It is unusual to create a durable non-auto-delete queue with a unique name. +This class has been deleted. +If you require its functionality, use `new Queue(UUID.randomUUID().toString())`. + +[[new-listener-container]] +== New Listener Container + +The `DirectMessageListenerContainer` has been added alongside the existing `SimpleMessageListenerContainer`. +See xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container] and xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for information about choosing which container to use as well as how to configure them. + + +[[log4j-appender]] +== Log4j Appender + +This appender is no longer available due to the end-of-life of log4j. +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for information about the available log appenders. + + +[[rabbittemplate-changes]] +== `RabbitTemplate` Changes + +IMPORTANT: Previously, a non-transactional `RabbitTemplate` participated in an existing transaction if it ran on a transactional listener container thread. +This was a serious bug. +However, users might have relied on this behavior. +Starting with version 1.6.2, you must set the `channelTransacted` boolean on the template for it to participate in the container transaction. + +The `RabbitTemplate` now uses a `DirectReplyToMessageListenerContainer` (by default) instead of creating a new consumer for each request. +See xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] for more information. + +The `AsyncRabbitTemplate` now supports direct reply-to. +See xref:amqp/request-reply.adoc#async-template[Async Rabbit Template] for more information. + +The `RabbitTemplate` and `AsyncRabbitTemplate` now have `receiveAndConvert` and `convertSendAndReceiveAsType` methods that take a `ParameterizedTypeReference` argument, letting the caller specify the type to which to convert the result. +This is particularly useful for complex types or when type information is not conveyed in message headers. +It requires a `SmartMessageConverter` such as the `Jackson2JsonMessageConverter`. +See xref:amqp/request-reply.adoc[Request/Reply Messaging], xref:amqp/request-reply.adoc#async-template[Async Rabbit Template], xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`], and xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`] for more information. + +You can now use a `RabbitTemplate` to perform multiple operations on a dedicated channel. +See xref:amqp/template.adoc#scoped-operations[Scoped Operations] for more information. + +[[listener-adapter]] +== Listener Adapter + +A convenient `FunctionalInterface` is available for using lambdas with the `MessageListenerAdapter`. +See xref:amqp/receiving-messages/async-consumer.adoc#message-listener-adapter[`MessageListenerAdapter`] for more information. + +[[listener-container-changes]] +== Listener Container Changes + +[[prefetch-default-value]] +=== Prefetch Default Value + +The prefetch default value used to be 1, which could lead to under-utilization of efficient consumers. +The default prefetch value is now 250, which should keep consumers busy in most common scenarios and, +thus, improve throughput. + +IMPORTANT: There are scenarios where the prefetch value should +be low -- for example, with large messages, especially if the processing is slow (messages could add up +to a large amount of memory in the client process), and if strict message ordering is necessary +(the prefetch value should be set back to 1 in this case). +Also, with low-volume messaging and multiple consumers (including concurrency within a single listener container instance), you may wish to reduce the prefetch to get a more even distribution of messages across consumers. + +For more background about prefetch, see this post about https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/[consumer utilization in RabbitMQ] +and this post about https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/[queuing theory]. + +[[message-count]] +=== Message Count + +Previously, `MessageProperties.getMessageCount()` returned `0` for messages emitted by the container. +This property applies only when you use `basicGet` (for example, from `RabbitTemplate.receive()` methods) and is now initialized to `null` for container messages. + +[[transaction-rollback-behavior]] +=== Transaction Rollback Behavior + +Message re-queue on transaction rollback is now consistent, regardless of whether or not a transaction manager is configured. +See xref:amqp/transactions.adoc#transaction-rollback[A note on Rollback of Received Messages] for more information. + +[[shutdown-behavior]] +=== Shutdown Behavior + +If the container threads do not respond to a shutdown within `shutdownTimeout`, the channels are forced closed by default. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[after-receive-message-post-processors]] +=== After Receive Message Post Processors + +If a `MessagePostProcessor` in the `afterReceiveMessagePostProcessors` property returns `null`, the message is discarded (and acknowledged if appropriate). + +[[connection-factory-changes]] +== Connection Factory Changes + +The connection and channel listener interfaces now provide a mechanism to obtain information about exceptions. +See xref:amqp/connections.adoc#connection-channel-listeners[Connection and Channel Listeners] and xref:amqp/template.adoc#publishing-is-async[Publishing is Asynchronous -- How to Detect Successes and Failures] for more information. + +A new `ConnectionNameStrategy` is now provided to populate the application-specific identification of the target RabbitMQ connection from the `AbstractConnectionFactory`. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +[[retry-changes]] +== Retry Changes + +The `MissingMessageIdAdvice` is no longer provided. +Its functionality is now built-in. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#retry[Failures in Synchronous Operations and Options for Retry] for more information. + +[[anonymous-queue-naming]] +== Anonymous Queue Naming + +By default, `AnonymousQueues` are now named with the default `Base64UrlNamingStrategy` instead of a simple `UUID` string. +See xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`] for more information. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +You can now provide simple queue declarations (bound only to the default exchange) in `@RabbitListener` annotations. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +You can now configure `@RabbitListener` annotations so that any exceptions are returned to the sender. +You can also configure a `RabbitListenerErrorHandler` to handle exceptions. +See xref:amqp/receiving-messages/async-annotation-driven/error-handling.adoc[Handling Exceptions] for more information. + +You can now bind a queue with multiple routing keys when you use the `@QueueBinding` annotation. +Also `@QueueBinding.exchange()` now supports custom exchange types and declares durable exchanges by default. + +You can now set the `concurrency` of the listener container at the annotation level rather than having to configure a different container factory for different concurrency settings. + +You can now set the `autoStartup` property of the listener container at the annotation level, overriding the default setting in the container factory. + +You can now set after receive and before send (reply) `MessagePostProcessor` instances in the `RabbitListener` container factories. + +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +Starting with version 2.0.3, one of the `@RabbitHandler` annotations on a class-level `@RabbitListener` can be designated as the default. +See xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[Multi-method Listeners] for more information. + +[[container-conditional-rollback]] +== Container Conditional Rollback + +When using an external transaction manager (such as JDBC), rule-based rollback is now supported when you provide the container with a transaction attribute. +It is also now more flexible when you use a transaction advice. +See xref:amqp/transactions.adoc#conditional-rollback[Conditional Rollback] for more information. + +[[remove-jackson-1-x-support]] +== Remove Jackson 1.x support + +Deprecated in previous versions, Jackson `1.x` converters and related components have now been deleted. +You can use similar components based on Jackson 2.x. +See xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. + +[[json-message-converter]] +== JSON Message Converter + +When the `__TypeId__` is set to `Hashtable` for an inbound JSON message, the default conversion type is now `LinkedHashMap`. +Previously, it was `Hashtable`. +To revert to a `Hashtable`, you can use `setDefaultMapType` on the `DefaultClassMapper`. + +[[xml-parsers]] +== XML Parsers + +When parsing `Queue` and `Exchange` XML components, the parsers no longer register the `name` attribute value as a bean alias if an `id` attribute is present. +See xref:amqp/broker-configuration.adoc#note-id-name[A Note On the `id` and `name` Attributes] for more information. + +[[blocked-connection]] +== Blocked Connection +You can now inject the `com.rabbitmq.client.BlockedListener` into the `org.springframework.amqp.rabbit.connection.Connection` object. +Also, the `ConnectionBlockedEvent` and `ConnectionUnblockedEvent` events are emitted by the `ConnectionFactory` when the connection is blocked or unblocked by the Broker. + +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc new file mode 100644 index 0000000000..07f3bacc9c --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc @@ -0,0 +1,127 @@ +[[changes-in-2-1-since-2-0]] += Changes in 2.1 Since 2.0 + +[[amqp-client-library]] +== AMQP Client library + +Spring AMQP now uses the 5.4.x version of the `amqp-client` library provided by the RabbitMQ team. +This client has auto-recovery configured by default. +See xref:amqp/connections.adoc#auto-recovery[RabbitMQ Automatic Connection/Topology recovery]. + +NOTE: As of version 4.0, the client enables automatic recovery by default. +While compatible with this feature, Spring AMQP has its own recovery mechanisms and the client recovery feature generally is not needed. +We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. +Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. +RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. + + +[[package-changes]] +== Package Changes + +Certain classes have moved to different packages. +Most are internal classes and do not affect user applications. +Two exceptions are `ChannelAwareMessageListener` and `RabbitListenerErrorHandler`. +These interfaces are now in `org.springframework.amqp.rabbit.listener.api`. + +[[publisher-confirms-changes]] +== Publisher Confirms Changes + +Channels enabled for publisher confirmations are not returned to the cache while there are outstanding confirmations. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +[[listener-container-factory-improvements]] +== Listener Container Factory Improvements + +You can now use the listener container factories to create any listener container, not only those for use with `@RabbitListener` annotations or the `@RabbitListenerEndpointRegistry`. +See xref:amqp/receiving-messages/using-container-factories.adoc[Using Container Factories] for more information. + +`ChannelAwareMessageListener` now inherits from `MessageListener`. + +[[broker-event-listener]] +== Broker Event Listener + +A `BrokerEventListener` is introduced to publish selected broker events as `ApplicationEvent` instances. +See xref:amqp/broker-events.adoc[Broker Event Listener] for more information. + +[[rabbitadmin-changes]] +== RabbitAdmin Changes + +The `RabbitAdmin` discovers beans of type `Declarables` (which is a container for `Declarable` - `Queue`, `Exchange`, and `Binding` objects) and declare the contained objects on the broker. +Users are discouraged from using the old mechanism of declaring `>` (and others) and should use `Declarables` beans instead. +By default, the old mechanism is disabled. +See xref:amqp/broker-configuration.adoc#collection-declaration[Declaring Collections of Exchanges, Queues, and Bindings] for more information. + +`AnonymousQueue` instances are now declared with `x-queue-master-locator` set to `client-local` by default, to ensure the queues are created on the node the application is connected to. +See xref:amqp/broker-configuration.adoc[Configuring the Broker] for more information. + +[[rabbittemplate-changes]] +== RabbitTemplate Changes + +You can now configure the `RabbitTemplate` with the `noLocalReplyConsumer` option to control a `noLocal` flag for reply consumers in the `sendAndReceive()` operations. +See xref:amqp/request-reply.adoc[Request/Reply Messaging] for more information. + +`CorrelationData` for publisher confirmations now has a `ListenableFuture`, which you can use to get the acknowledgment instead of using a callback. +When returns and confirmations are enabled, the correlation data, if provided, is populated with the returned message. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +A method called `replyTimedOut` is now provided to notify subclasses that a reply has timed out, allowing for any state cleanup. +See xref:amqp/request-reply.adoc#reply-timeout[Reply Timeout] for more information. + +You can now specify an `ErrorHandler` to be invoked when using request/reply with a `DirectReplyToMessageListenerContainer` (the default) when exceptions occur when replies are delivered (for example, late replies). +See `setReplyErrorHandler` on the `RabbitTemplate`. +(Also since 2.0.11). + +[[message-conversion]] +== Message Conversion + +We introduced a new `Jackson2XmlMessageConverter` to support converting messages from and to XML format. +See xref:amqp/message-converters.adoc#jackson2xml[`Jackson2XmlMessageConverter`] for more information. + +[[management-rest-api]] +== Management REST API + +The `RabbitManagementTemplate` is now deprecated in favor of the direct `com.rabbitmq.http.client.Client` (or `com.rabbitmq.http.client.ReactorNettyClient`) usage. +See xref:amqp/management-rest-api.adoc#management-rest-api[RabbitMQ REST API] for more information. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +The listener container factory can now be configured with a `RetryTemplate` and, optionally, a `RecoveryCallback` used when sending replies. +See xref:amqp/receiving-messages/async-annotation-driven/enable.adoc[Enable Listener Endpoint Annotations] for more information. + +[[async-rabbitlistener-return]] +== Async `@RabbitListener` Return + +`@RabbitListener` methods can now return `ListenableFuture` or `Mono`. +See xref:amqp/receiving-messages/async-returns.adoc[Asynchronous `@RabbitListener` Return Types] for more information. + +[[connection-factory-bean-changes]] +== Connection Factory Bean Changes + +By default, the `RabbitConnectionFactoryBean` now calls `enableHostnameVerification()`. +To revert to the previous behavior, set the `enableHostnameVerification` property to `false`. + +[[connection-factory-changes]] +== Connection Factory Changes + +The `CachingConnectionFactory` now unconditionally disables auto-recovery in the underlying RabbitMQ `ConnectionFactory`, even if a pre-configured instance is provided in a constructor. +While steps have been taken to make Spring AMQP compatible with auto recovery, certain corner cases have arisen where issues remain. +Spring AMQP has had its own recovery mechanism since 1.0.0 and does not need to use the recovery provided by the client. +While it is still possible to enable the feature (using `cachingConnectionFactory.getRabbitConnectionFactory()` `.setAutomaticRecoveryEnabled()`) after the `CachingConnectionFactory` is constructed, **we strongly recommend that you not do so**. +We recommend that you use a separate RabbitMQ `ConnectionFactory` if you need auto recovery connections when using the client factory directly (rather than using Spring AMQP components). + +[[listener-container-changes]] +== Listener Container Changes + +The default `ConditionalRejectingErrorHandler` now completely discards messages that cause fatal errors if an `x-death` header is present. +See xref:amqp/exception-handling.adoc[Exception Handling] for more information. + +[[immediate-requeue]] +== Immediate requeue + +A new `ImmediateRequeueAmqpException` is introduced to notify a listener container that the message has to be re-queued. +To use this feature, a new `ImmediateRequeueMessageRecoverer` implementation is added. + +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case] for more information. + + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc new file mode 100644 index 0000000000..90e38330b8 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc @@ -0,0 +1,137 @@ +[[changes-in-2-2-since-2-1]] += Changes in 2.2 Since 2.1 + +This section describes the changes between version 2.1 and version 2.2. + +[[package-changes]] +== Package Changes + +The following classes/interfaces have been moved from `org.springframework.amqp.rabbit.core.support` to `org.springframework.amqp.rabbit.batch`: + +* `BatchingStrategy` +* `MessageBatch` +* `SimpleBatchingStrategy` + +In addition, `ListenerExecutionFailedException` has been moved from `org.springframework.amqp.rabbit.listener.exception` to `org.springframework.amqp.rabbit.support`. + +[[dependency-changes]] +== Dependency Changes + +JUnit (4) is now an optional dependency and will no longer appear as a transitive dependency. + +The `spring-rabbit-junit` module is now a *compile* dependency in the `spring-rabbit-test` module for a better target application development experience when with only a single `spring-rabbit-test` we get the full stack of testing utilities for AMQP components. + +[[-breaking-api-changes]] +== "Breaking" API Changes + +the JUnit (5) `RabbitAvailableCondition.getBrokerRunning()` now returns a `BrokerRunningSupport` instance instead of a `BrokerRunning`, which depends on JUnit 4. +It has the same API so it's just a matter of changing the class name of any references. +See xref:testing.adoc#junit5-conditions[JUnit5 Conditions] for more information. + +[[listenercontainer-changes]] +== ListenerContainer Changes + +Messages with fatal exceptions are now rejected and NOT requeued, by default, even if the acknowledge mode is manual. +See xref:amqp/exception-handling.adoc[Exception Handling] for more information. + +Listener performance can now be monitored using Micrometer `Timer` s. +See xref:amqp/receiving-messages/micrometer.adoc[Monitoring Listener Performance] for more information. + +[[rabbitlistener-changes]] +== @RabbitListener Changes + +You can now configure an `executor` on each listener, overriding the factory configuration, to more easily identify threads associated with the listener. +You can now override the container factory's `acknowledgeMode` property with the annotation's `ackMode` property. +See xref:amqp/receiving-messages/async-annotation-driven/enable.adoc#listener-property-overrides[overriding container factory properties] for more information. + +When using xref:amqp/receiving-messages/batch.adoc[batching], `@RabbitListener` methods can now receive a complete batch of messages in one call instead of getting them one-at-a-time. + +When receiving batched messages one-at-a-time, the last message has the `isLastInBatch` message property set to true. + +In addition, received batched messages now contain the `amqp_batchSize` header. + +Listeners can also consume batches created in the `SimpleMessageListenerContainer`, even if the batch is not created by the producer. +See xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container] for more information. + +Spring Data Projection interfaces are now supported by the `Jackson2JsonMessageConverter`. +See xref:amqp/message-converters.adoc#data-projection[Using Spring Data Projection Interfaces] for more information. + +The `Jackson2JsonMessageConverter` now assumes the content is JSON if there is no `contentType` property, or it is the default (`application/octet-string`). +See xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-from-message[Converting from a `Message`] for more information. + +Similarly. the `Jackson2XmlMessageConverter` now assumes the content is XML if there is no `contentType` property, or it is the default (`application/octet-string`). +See xref:amqp/message-converters.adoc#jackson2xml[`Jackson2XmlMessageConverter`] for more information. + +When a `@RabbitListener` method returns a result, the bean and `Method` are now available in the reply message properties. +This allows configuration of a `beforeSendReplyMessagePostProcessor` to, for example, set a header in the reply to indicate which method was invoked on the server. +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for more information. + +You can now configure a `ReplyPostProcessor` to make modifications to a reply message before it is sent. +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for more information. + +[[amqp-logging-appenders-changes]] +== AMQP Logging Appenders Changes + +The Log4J and Logback `AmqpAppender` s now support a `verifyHostname` SSL option. + +Also these appenders now can be configured to not add MDC entries as headers. +The `addMdcAsHeaders` boolean option has been introduces to configure such a behavior. + +The appenders now support the `SaslConfig` property. + +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for more information. + +[[messagelisteneradapter-changes]] +== MessageListenerAdapter Changes + +The `MessageListenerAdapter` provides now a new `buildListenerArguments(Object, Channel, Message)` method to build an array of arguments to be passed into target listener and an old one is deprecated. +See xref:amqp/receiving-messages/async-consumer.adoc#message-listener-adapter[`MessageListenerAdapter`] for more information. + +[[exchange/queue-declaration-changes]] +== Exchange/Queue Declaration Changes + +The `ExchangeBuilder` and `QueueBuilder` fluent APIs used to create `Exchange` and `Queue` objects for declaration by `RabbitAdmin` now support "well known" arguments. +See xref:amqp/broker-configuration.adoc#builder-api[Builder API for Queues and Exchanges] for more information. + +The `RabbitAdmin` has a new property `explicitDeclarationsOnly`. +See xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration] for more information. + +[[connection-factory-changes]] +== Connection Factory Changes + +The `CachingConnectionFactory` has a new property `shuffleAddresses`. +When providing a list of broker node addresses, the list will be shuffled before creating a connection so that the order in which the connections are attempted is random. +See xref:amqp/connections.adoc#cluster[Connecting to a Cluster] for more information. + +When using Publisher confirms and returns, the callbacks are now invoked on the connection factory's `executor`. +This avoids a possible deadlock in the `amqp-clients` library if you perform rabbit operations from within the callback. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +Also, the publisher confirm type is now specified with the `ConfirmType` enum instead of the two mutually exclusive setter methods. + +The `RabbitConnectionFactoryBean` now uses TLS 1.2 by default when SSL is enabled. +See xref:amqp/connections.adoc#rabbitconnectionfactorybean-configuring-ssl[`RabbitConnectionFactoryBean` and Configuring SSL] for more information. + +[[new-messagepostprocessor-classes]] +== New MessagePostProcessor Classes + +Classes `DeflaterPostProcessor` and `InflaterPostProcessor` were added to support compression and decompression, respectively, when the message content-encoding is set to `deflate`. + +[[other-changes]] +== Other Changes + +The `Declarables` object (for declaring multiple queues, exchanges, bindings) now has a filtered getter for each type. +See xref:amqp/broker-configuration.adoc#collection-declaration[Declaring Collections of Exchanges, Queues, and Bindings] for more information. + +You can now customize each `Declarable` bean before the `RabbitAdmin` processes the declaration thereof. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#automatic-declaration[Automatic Declaration of Exchanges, Queues, and Bindings] for more information. + +`singleActiveConsumer()` has been added to the `QueueBuilder` to set the `x-single-active-consumer` queue argument. +See xref:amqp/broker-configuration.adoc#builder-api[Builder API for Queues and Exchanges] for more information. + +Outbound headers with values of type `Class` are now mapped using `getName()` instead of `toString()`. +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +Recovery of failed producer-created batches is now supported. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#batch-retry[Retry with Batch Listeners] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc new file mode 100644 index 0000000000..fca45e9164 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc @@ -0,0 +1,72 @@ +[[changes-in-2-3-since-2-2]] += Changes in 2.3 Since 2.2 + +This section describes the changes between version 2.2 and version 2.3. +See xref:appendix/change-history.adoc[Change History] for changes in previous versions. + +[[connection-factory-changes]] +== Connection Factory Changes + +Two additional connection factories are now provided. +See xref:amqp/connections.adoc#choosing-factory[Choosing a Connection Factory] for more information. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +You can now specify a reply content type. +See xref:amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc[Reply ContentType] for more information. + +[[message-converter-changes]] +== Message Converter Changes + +The `Jackson2JMessageConverter` s can now deserialize abstract classes (including interfaces) if the `ObjectMapper` is configured with a custom deserializer. +See xref:amqp/message-converters.adoc#jackson-abstract[Deserializing Abstract Classes] for more information. + +[[testing-changes]] +== Testing Changes + +A new annotation `@SpringRabbitTest` is provided to automatically configure some infrastructure beans for when you are not using `SpringBootTest`. +See xref:testing.adoc#spring-rabbit-test[@SpringRabbitTest] for more information. + +[[rabbittemplate-changes]] +== RabbitTemplate Changes + +The template's `ReturnCallback` has been refactored as `ReturnsCallback` for simpler use in lambda expressions. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +When using returns and correlated confirms, the `CorrelationData` now requires a unique `id` property. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +When using direct reply-to, you can now configure the template such that the server does not need to return correlation data with the reply. +See xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] for more information. + +[[listener-container-changes]] +== Listener Container Changes + +A new listener container property `consumeDelay` is now available; it is helpful when using the {rabbitmq-server-github}/rabbitmq_sharding[RabbitMQ Sharding Plugin]. + +The default `JavaLangErrorHandler` now calls `System.exit(99)`. +To revert to the previous behavior (do nothing), add a no-op handler. + +The containers now support the `globalQos` property to apply the `prefetchCount` globally for the channel rather than for each consumer on the channel. + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[messagepostprocessor-changes]] +== MessagePostProcessor Changes + +The compressing `MessagePostProcessor` s now use a comma to separate multiple content encodings instead of a colon. +The decompressors can handle both formats but, if you produce messages with this version that are consumed by versions earlier than 2.2.12, you should configure the compressor to use the old delimiter. +See the IMPORTANT note in xref:amqp/post-processing.adoc[Modifying Messages - Compression and More] for more information. + +[[multiple-broker-support-improvements]] +== Multiple Broker Support Improvements + +See xref:amqp/multi-rabbit.adoc[Multiple Broker (or Cluster) Support] for more information. + +[[republishmessagerecoverer-changes]] +== RepublishMessageRecoverer Changes + +A new subclass of this recoverer is not provided that supports publisher confirms. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc new file mode 100644 index 0000000000..372f46fcff --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc @@ -0,0 +1,36 @@ +[[changes-in-2-4-since-2-3]] += Changes in 2.4 Since 2.3 +:page-section-summary-toc: 1 + +This section describes the changes between version 2.3 and version 2.4. +See xref:appendix/change-history.adoc[Change History] for changes in previous versions. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +`MessageProperties` is now available for argument matching. +See xref:amqp/receiving-messages/async-annotation-driven/enable-signature.adoc[Annotated Endpoint Method Signature] for more information. + +[[rabbitadmin-changes]] +== `RabbitAdmin` Changes + +A new property `recoverManualDeclarations` allows recovery of manually declared queues/exchanges/bindings. +See xref:amqp/broker-configuration.adoc#declarable-recovery[Recovering Auto-Delete Declarations] for more information. + +[[remoting-support]] +== Remoting Support + +Support remoting using Spring Framework’s RMI support is deprecated and will be removed in 3.0. See Spring Remoting with AMQP for more information. + +[[stream-support-changes]] +== Stream Support Changes + +`RabbitStreamOperations` and `RabbitStreamTemplate` have been deprecated in favor of `RabbitStreamOperations2` and `RabbitStreamTemplate2` respectively; they return `CompletableFuture` instead of `ListenableFuture`. +See xref:stream.adoc[Using the RabbitMQ Stream Plugin] for more information. + +[[message-converter-changes]] +== Message Converter Changes + +The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. +See xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc new file mode 100644 index 0000000000..63562059c8 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc @@ -0,0 +1,70 @@ +[[changes-in-3-0-since-2-4]] += Changes in 3.0 Since 2.4 + +[[java-17-spring-framework-6-0]] +== Java 17, Spring Framework 6.0 + +This version requires Spring Framework 6.0 and Java 17 + +[[remoting]] +== Remoting + +The remoting feature (using RMI) is no longer supported. + +[[observation]] +== Observation + +Enabling observation for timers and tracing using Micrometer is now supported. +See xref:stream.adoc#stream-micrometer-observation[Micrometer Observation] for more information. + +[[x30-Native]] +== Native Images + +Support for creating native images is provided. +See xref:appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc#x30-Native[Native Images] for more information. + +[[asyncrabbittemplate]] +== AsyncRabbitTemplate + +IMPORTANT: The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. +See xref:amqp/request-reply.adoc#async-template[Async Rabbit Template] for more information. + +[[stream-support-changes]] +== Stream Support Changes + +IMPORTANT: `RabbitStreamOperations` and `RabbitStreamTemplate` methods now return `CompletableFuture` instead of `ListenableFuture`. + +Super streams and single active consumers thereon are now supported. + +See xref:stream.adoc[Using the RabbitMQ Stream Plugin] for more information. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +Batch listeners can now consume `Collection` as well as `List`. +The batch messaging adapter now ensures that the method is suitable for consuming batches. +When setting the container factory `consumerBatchEnabled` to `true`, the `batchListener` property is also set to `true`. +See xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching] for more information. + +`MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`. +See xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-from-message[Converting from a `Message`] for more information + +You can now configure a `ReplyPostProcessor` via the container factory rather than via a property on `@RabbitListener`. +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for more information. + +The `@RabbitListener` (and `@RabbitHandler`) methods can now be declared as Kotlin `suspend` functions. +See xref:amqp/receiving-messages/async-returns.adoc[Asynchronous `@RabbitListener` Return Types] for more information. + +Starting with version 3.0.5, listeners with async return types (including Kotlin suspend functions) invoke the `RabbitListenerErrorHandler` (if configured) after a failure. +Previously, the error handler was only invoked with synchronous invocations. + +[[connection-factory-changes]] +== Connection Factory Changes + +The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. +This results in connecting to a random host when multiple addresses are provided. +See xref:amqp/connections.adoc#cluster[Connecting to a Cluster] for more information. + +The `LocalizedQueueConnectionFactory` no longer uses the RabbitMQ `http-client` library to determine which node is the leader for a queue. +See xref:amqp/connections.adoc#queue-affinity[Queue Affinity and the `LocalizedQueueConnectionFactory`] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc new file mode 100644 index 0000000000..171b94d77c --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc @@ -0,0 +1,24 @@ +[[changes-in-3-1-since-3-0]] += Changes in 3.1 Since 3.0 + +[[java-17-spring-framework-6-1]] +== Java 17, Spring Framework 6.1 + +This version requires Spring Framework 6.1 and Java 17. + +[[x31-exc]] +== Exclusive Consumer Logging + +Log messages reporting access refusal due to exclusive consumers are now logged at DEBUG level by default. +It remains possible to configure your own logging behavior by setting the `exclusiveConsumerExceptionLogger` and `closeExceptionLogger` properties on the listener container and connection factory respectively. +In addition, the `SimpleMessageListenerContainer` consumer restart after such an exception is now logged at DEBUG level by default (previously INFO). +A new method `logRestart()` has been added to the `ConditionalExceptionLogger` to allow this to be changed. +See xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events] and xref:amqp/connections.adoc#channel-close-logging[Logging Channel Close Events] for more information. + +[[x31-conn-backoff]] +== Connections Enhancement + +Connection Factory supported backoff policy when creating connection channel. +See xref:amqp/connections.adoc[Choosing a Connection Factory] for more information. + + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-2-since-3-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-2-since-3-1.adoc new file mode 100644 index 0000000000..cecbc5f8ec --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-2-since-3-1.adoc @@ -0,0 +1,17 @@ +[[changes-in-3-2-since-3-1]] += Changes in 3.2 Since 3.1 + +[[spring-framework-6-2]] +== Spring Framework 6.1 + +This version requires Spring Framework 6.2. + +[[x32-consistent-hash-exchange]] +== Consistent Hash Exchange + +The convenient `ConsistentHashExchange` and respective `ExchangeBuilder.consistentHashExchange()` API has been introduced. + +[[x32-retry-count-header]] +== The `retry_count` header + +The `retry_count` header should be used now instead of relying on server side increment for the `x-death.count` property. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-1-since-1-0.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-1-since-1-0.adoc new file mode 100644 index 0000000000..b74f52de59 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-1-since-1-0.adoc @@ -0,0 +1,21 @@ +[[changes-to-1-1-since-1-0]] += Changes to 1.1 Since 1.0 +:page-section-summary-toc: 1 + +[[general]] +== General + +Spring-AMQP is now built with Gradle. + +Adds support for publisher confirms and returns. + +Adds support for HA queues and broker failover. + +Adds support for dead letter exchanges and dead letter queues. + +[[amqp-log4j-appender]] +== AMQP Log4j Appender + +Adds an option to support adding a message ID to logged messages. + +Adds an option to allow the specification of a `Charset` name to be used when converting `String` to `byte[]`. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc new file mode 100644 index 0000000000..4180c54d78 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc @@ -0,0 +1,58 @@ +[[changes-to-1-2-since-1-1]] += Changes to 1.2 Since 1.1 + +[[rabbitmq-version]] +== RabbitMQ Version + +Spring AMQP now uses RabbitMQ 3.1.x by default (but retains compatibility with earlier versions). +Certain deprecations have been added for features no longer supported by RabbitMQ 3.1.x -- federated exchanges and the `immediate` property on the `RabbitTemplate`. + +[[rabbit-admin]] +== Rabbit Admin + +`RabbitAdmin` now provides an option to let exchange, queue, and binding declarations continue when a declaration fails. +Previously, all declarations stopped on a failure. +By setting `ignore-declaration-exceptions`, such exceptions are logged (at the `WARN` level), but further declarations continue. +An example where this might be useful is when a queue declaration fails because of a slightly different `ttl` setting that would normally stop other declarations from proceeding. + +`RabbitAdmin` now provides an additional method called `getQueueProperties()`. +You can use this determine if a queue exists on the broker (returns `null` for a non-existent queue). +In addition, it returns the current number of messages in the queue as well as the current number of consumers. + +[[rabbit-template]] +== Rabbit Template + +Previously, when the `...sendAndReceive()` methods were used with a fixed reply queue, two custom headers were used for correlation data and to retain and restore reply queue information. +With this release, the standard message property (`correlationId`) is used by default, although you can specify a custom property to use instead. +In addition, nested `replyTo` information is now retained internally in the template, instead of using a custom header. + +The `immediate` property is deprecated. +You must not set this property when using RabbitMQ 3.0.x or greater. + +[[json-message-converters]] +== JSON Message Converters + +A Jackson 2.x `MessageConverter` is now provided, along with the existing converter that uses Jackson 1.x. + +[[automatic-declaration-of-queues-and-other-items]] +== Automatic Declaration of Queues and Other Items + +Previously, when declaring queues, exchanges and bindings, you could not define which connection factory was used for the declarations. +Each `RabbitAdmin` declared all components by using its connection. + +Starting with this release, you can now limit declarations to specific `RabbitAdmin` instances. +See xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration]. + +[[amqp-remoting]] +== AMQP Remoting + +Facilities are now provided for using Spring remoting techniques, using AMQP as the transport for the RPC calls. +For more information see xref:amqp/request-reply.adoc#remoting[Spring Remoting with AMQP]. + +[[requested-heart-beats]] +== Requested Heart Beats + +Several users have asked for the underlying client connection factory's `requestedHeartBeats` property to be exposed on the Spring AMQP `CachingConnectionFactory`. +This is now available. +Previously, it was necessary to configure the AMQP client factory as a separate bean and provide a reference to it in the `CachingConnectionFactory`. + diff --git a/src/reference/asciidoc/further-reading.adoc b/src/reference/antora/modules/ROOT/pages/further-reading.adoc similarity index 94% rename from src/reference/asciidoc/further-reading.adoc rename to src/reference/antora/modules/ROOT/pages/further-reading.adoc index 37c01eea7c..d199110deb 100644 --- a/src/reference/asciidoc/further-reading.adoc +++ b/src/reference/antora/modules/ROOT/pages/further-reading.adoc @@ -1,5 +1,6 @@ [[further-reading]] -=== Further Reading += Further Reading +:page-section-summary-toc: 1 For those who are not familiar with AMQP, the https://www.amqp.org/resources/download[specification] is actually quite readable. It is, of course, the authoritative source of information, and the Spring AMQP code should be easy to understand for anyone who is familiar with the spec. diff --git a/src/reference/asciidoc/preface.adoc b/src/reference/antora/modules/ROOT/pages/index.adoc similarity index 65% rename from src/reference/asciidoc/preface.adoc rename to src/reference/antora/modules/ROOT/pages/index.adoc index 5f07f478e1..1648e79ad4 100644 --- a/src/reference/asciidoc/preface.adoc +++ b/src/reference/antora/modules/ROOT/pages/index.adoc @@ -1,7 +1,14 @@ +[[spring-amqp-reference]] += Spring AMQP +:numbered: +:icons: font +:hide-uri-scheme: +Mark Pollack; Mark Fisher; Oleg Zhurakousky; Dave Syer; Gary Russell; Gunnar Hillert; Artem Bilan; Stéphane Nicoll; Arnaud Cogoluègnes; Jay Bryant + [[preface]] The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. We provide a "`template`" as a high-level abstraction for sending and receiving messages. We also provide support for message-driven POJOs. These libraries facilitate management of AMQP resources while promoting the use of dependency injection and declarative configuration. In all of these cases, you can see similarities to the JMS support in the Spring Framework. -For other project-related information, visit the Spring AMQP project https://projects.spring.io/spring-amqp/[homepage]. +For other project-related information, visit the Spring AMQP project https://projects.spring.io/spring-amqp/[homepage]. \ No newline at end of file diff --git a/src/reference/asciidoc/si-amqp.adoc b/src/reference/antora/modules/ROOT/pages/integration-reference.adoc similarity index 78% rename from src/reference/asciidoc/si-amqp.adoc rename to src/reference/antora/modules/ROOT/pages/integration-reference.adoc index c52c40c6aa..1dbc762022 100644 --- a/src/reference/asciidoc/si-amqp.adoc +++ b/src/reference/antora/modules/ROOT/pages/integration-reference.adoc @@ -1,41 +1,40 @@ -[[spring-integration-amqp]] -=== Spring Integration AMQP Support +[[spring-integration-reference]] += Spring Integration - Reference -This brief chapter covers the relationship between the Spring Integration and the Spring AMQP projects. +This part of the reference documentation provides a quick introduction to the AMQP support within the Spring Integration project. [[spring-integration-amqp-introduction]] -==== Introduction +== Introduction -The https://www.springsource.org/spring-integration[Spring Integration] project includes AMQP Channel Adapters and Gateways that build upon the Spring AMQP project. +The https://spring.io/spring-integration[Spring Integration] project includes AMQP Channel Adapters and Gateways that build upon the Spring AMQP project. Those adapters are developed and released in the Spring Integration project. In Spring Integration, "`Channel Adapters`" are unidirectional (one-way), whereas "`Gateways`" are bidirectional (request-reply). We provide an inbound-channel-adapter, an outbound-channel-adapter, an inbound-gateway, and an outbound-gateway. Since the AMQP adapters are part of the Spring Integration release, the documentation is available as part of the Spring Integration distribution. We provide a quick overview of the main features here. -See the https://docs.spring.io/spring-integration/reference/htmlsingle/[Spring Integration Reference Guide] for much more detail. +See the {spring-integration-docs}[Spring Integration Reference Guide] for much more detail. -==== Inbound Channel Adapter +[[inbound-channel-adapter]] +== Inbound Channel Adapter To receive AMQP Messages from a queue, you can configure an ``. The following example shows how to configure an inbound channel adapter: -==== [source,xml] ---- ---- -==== -==== Outbound Channel Adapter +[[outbound-channel-adapter]] +== Outbound Channel Adapter To send AMQP Messages to an exchange, you can configure an ``. You can optionally provide a 'routing-key' in addition to the exchange name. The following example shows how to define an outbound channel adapter: -==== [source,xml] ---- ---- -==== -==== Inbound Gateway +[[inbound-gateway]] +== Inbound Gateway To receive an AMQP Message from a queue and respond to its reply-to address, you can configure an ``. The following example shows how to define an inbound gateway: -==== [source,xml] ---- ---- -==== -==== Outbound Gateway +[[outbound-gateway]] +== Outbound Gateway To send AMQP Messages to an exchange and receive back a response from a remote client, you can configure an ``. You can optionally provide a 'routing-key' in addition to the exchange name. The following example shows how to define an outbound gateway: -==== [source,xml] ---- ---- -==== diff --git a/src/reference/antora/modules/ROOT/pages/introduction/index.adoc b/src/reference/antora/modules/ROOT/pages/introduction/index.adoc new file mode 100644 index 0000000000..b88a71a602 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/introduction/index.adoc @@ -0,0 +1,5 @@ +[[introduction]] += Introduction + +This first part of the reference documentation is a high-level overview of Spring AMQP and the underlying concepts. +It includes some code snippets to get you up and running as quickly as possible. diff --git a/src/reference/asciidoc/quick-tour.adoc b/src/reference/antora/modules/ROOT/pages/introduction/quick-tour.adoc similarity index 90% rename from src/reference/asciidoc/quick-tour.adoc rename to src/reference/antora/modules/ROOT/pages/introduction/quick-tour.adoc index af2aa94e2a..21736104d4 100644 --- a/src/reference/asciidoc/quick-tour.adoc +++ b/src/reference/antora/modules/ROOT/pages/introduction/quick-tour.adoc @@ -1,7 +1,8 @@ [[quick-tour]] -=== Quick Tour for the impatient += Quick Tour for the impatient -==== Introduction +[[introduction]] +== Introduction This is the five-minute tour to get started with Spring AMQP. @@ -9,7 +10,6 @@ Prerequisites: Install and run the RabbitMQ broker (https://www.rabbitmq.com/dow Then grab the spring-rabbit JAR and all its dependencies - the easiest way to do so is to declare a dependency in your build tool. For example, for Maven, you can do something resembling the following: -==== [source,xml,subs="+attributes"] ---- @@ -18,31 +18,30 @@ For example, for Maven, you can do something resembling the following: {project-version} ---- -==== For Gradle, you can do something resembling the following: -==== [source,groovy,subs="+attributes"] ---- compile 'org.springframework.amqp:spring-rabbit:{project-version}' ---- -==== [[compatibility]] -===== Compatibility +=== Compatibility -The minimum Spring Framework version dependency is 5.2.0. +The minimum Spring Framework version dependency is 6.1.0. -The minimum `amqp-client` Java client library version is 5.7.0. +The minimum `amqp-client` Java client library version is 5.18.0. -===== Very, Very Quick +The minimum `stream-client` Java client library for stream queues is 0.12.0. + +[[very-very-quick]] +=== Very, Very Quick This section offers the fastest introduction. First, add the following `import` statements to make the examples later in this section work: -==== [source, java] ---- import org.springframework.amqp.core.AmqpAdmin; @@ -53,11 +52,9 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; ---- -==== The following example uses plain, imperative Java to send and receive a message: -==== [source,java] ---- ConnectionFactory connectionFactory = new CachingConnectionFactory(); @@ -67,7 +64,6 @@ AmqpTemplate template = new RabbitTemplate(connectionFactory); template.convertAndSend("myqueue", "foo"); String foo = (String) template.receiveAndConvert("myqueue"); ---- -==== Note that there is also a `ConnectionFactory` in the native Java Rabbit client. We use the Spring abstraction in the preceding code. @@ -75,11 +71,11 @@ It caches channels (and optionally connections) for reuse. We rely on the default exchange in the broker (since none is specified in the send), and the default binding of all queues to the default exchange by their name (thus, we can use the queue name as a routing key in the send). Those behaviors are defined in the AMQP specification. -===== With XML Configuration +[[with-xml-configuration]] +=== With XML Configuration The following example is the same as the preceding example but externalizes the resource configuration to XML: -==== [source,java] ---- ApplicationContext context = @@ -109,18 +105,17 @@ String foo = (String) template.receiveAndConvert("myqueue"); ---- -==== By default, the `` declaration automatically looks for beans of type `Queue`, `Exchange`, and `Binding` and declares them to the broker on behalf of the user. As a result, you need not use that bean explicitly in the simple Java driver. There are plenty of options to configure the properties of the components in the XML schema. You can use auto-complete features of your XML editor to explore them and look at their documentation. -===== With Java Configuration +[[with-java-configuration]] +=== With Java Configuration The following example repeats the same example as the preceding example but with the external configuration defined in Java: -==== [source,java] ---- ApplicationContext context = @@ -155,13 +150,12 @@ public class RabbitConfiguration { } } ---- -==== -===== With Spring Boot Auto Configuration and an Async POJO Listener +[[with-spring-boot-auto-configuration-and-an-async-pojo-listener]] +=== With Spring Boot Auto Configuration and an Async POJO Listener Spring Boot automatically configures the infrastructure beans, as the following example shows: -==== [source, java] ---- @SpringBootApplication @@ -188,4 +182,3 @@ public class Application { } ---- -==== diff --git a/src/reference/asciidoc/logging.adoc b/src/reference/antora/modules/ROOT/pages/logging.adoc similarity index 92% rename from src/reference/asciidoc/logging.adoc rename to src/reference/antora/modules/ROOT/pages/logging.adoc index fe07bf4d69..cf9b0ff0e5 100644 --- a/src/reference/asciidoc/logging.adoc +++ b/src/reference/antora/modules/ROOT/pages/logging.adoc @@ -1,5 +1,5 @@ [[logging]] -=== Logging Subsystem AMQP Appenders += Logging Subsystem AMQP Appenders The framework provides logging appenders for some popular logging subsystems: @@ -8,7 +8,8 @@ The framework provides logging appenders for some popular logging subsystems: The appenders are configured by using the normal mechanisms for the logging subsystem, available properties are specified in the following sections. -==== Common properties +[[common-properties]] +== Common properties The following properties are available with all appenders: @@ -34,7 +35,7 @@ See `declareExchange`. | applicationId | -| Application ID -- added to the routing key if the pattern includes `%X{applicationId}`. +| Application ID -- added to the routing key if the pattern includes `+%X{applicationId}+`. | senderPoolSize | 2 @@ -72,12 +73,12 @@ Retries are delayed as follows: `N ^ log(N)`, where `N` is the retry number. | useSsl | false | Whether to use SSL for the RabbitMQ connection. -See <> +See xref:amqp/connections.adoc#rabbitconnectionfactorybean-configuring-ssl[`RabbitConnectionFactoryBean` and Configuring SSL] | verifyHostname | true | Enable server hostname verification for TLS connections. -See <> +See xref:amqp/connections.adoc#rabbitconnectionfactorybean-configuring-ssl[`RabbitConnectionFactoryBean` and Configuring SSL] | sslAlgorithm | null @@ -165,11 +166,11 @@ Please note, the `JsonLayout` adds MDC into the message by default. |=== -==== Log4j 2 Appender +[[log4j-2-appender]] +== Log4j 2 Appender The following example shows how to configure a Log4j 2 appender: -==== [source, xml] ---- @@ -185,7 +186,6 @@ The following example shows how to configure a Log4j 2 appender: ---- -==== [IMPORTANT] ==== @@ -198,11 +198,11 @@ One way to do that is to set the system property `-Dlog4j2.enable.threadlocals=f If you use asynchronous publishing with the `ReusableLogEventFactory`, events have a high likelihood of being corrupted due to cross-talk. ==== -==== Logback Appender +[[logback-appender]] +== Logback Appender The following example shows how to configure a logback appender: -==== [source, xml] ---- @@ -222,7 +222,6 @@ The following example shows how to configure a logback appender: false ---- -==== Starting with version 1.7.1, the Logback `AmqpAppender` provides an `includeCallerData` option, which is `false` by default. Extracting caller data can be rather expensive, because the log event has to create a throwable and inspect it to determine the calling location. @@ -232,7 +231,8 @@ You can configure the appender to include caller data by setting the `includeCal Starting with version 2.0.0, the Logback `AmqpAppender` supports https://logback.qos.ch/manual/encoders.html[Logback encoders] with the `encoder` option. The `encoder` and `layout` options are mutually exclusive. -==== Customizing the Messages +[[customizing-the-messages]] +== Customizing the Messages By default, AMQP appenders populate the following message properties: @@ -255,7 +255,6 @@ Each of the appenders can be subclassed, letting you modify the messages before The following example shows how to customize log messages: -==== [source, java] ---- public class MyEnhancedAppender extends AmqpAppender { @@ -268,12 +267,10 @@ public class MyEnhancedAppender extends AmqpAppender { } ---- -==== Starting with 2.2.4, the log4j2 `AmqpAppender` can be extended using `@PluginBuilderFactory` and extending also `AmqpAppender.Builder` -==== [source, java] ---- @Plugin(name = "MyEnhancedAppender", category = "Core", elementType = "appender", printObject = true) @@ -305,21 +302,21 @@ public class MyEnhancedAppender extends AmqpAppender { } ---- -==== -==== Customizing the Client Properties +[[customizing-the-client-properties]] +== Customizing the Client Properties You can add custom client properties by adding either string properties or more complex properties. -===== Simple String Properties +[[simple-string-properties]] +=== Simple String Properties Each appender supports adding client properties to the RabbitMQ connection. The following example shows how to add a custom client property for logback: -==== [source, xml] ---- @@ -328,10 +325,8 @@ The following example shows how to add a custom client property for logback: ... ---- -==== .log4j2 -==== [source, xml] ---- @@ -343,20 +338,19 @@ The following example shows how to add a custom client property for logback: ---- -==== The properties are a comma-delimited list of `key:value` pairs. Keys and values cannot contain commas or colons. These properties appear on the RabbitMQ Admin UI when the connection is viewed. -===== Advanced Technique for Logback +[[advanced-technique-for-logback]] +=== Advanced Technique for Logback You can subclass the Logback appender. Doing so lets you modify the client connection properties before the connection is established. The following example shows how to do so: -==== [source, java] ---- public class MyEnhancedAppender extends AmqpAppender { @@ -374,14 +368,14 @@ public class MyEnhancedAppender extends AmqpAppender { } ---- -==== Then you can add `thing2` to logback.xml. For String properties such as those shown in the preceding example, the previous technique can be used. Subclasses allow for adding richer properties (such as adding a `Map` or numeric property). -==== Providing a Custom Queue Implementation +[[providing-a-custom-queue-implementation]] +== Providing a Custom Queue Implementation The `AmqpAppenders` use a `BlockingQueue` to asynchronously publish logging events to RabbitMQ. By default, a `LinkedBlockingQueue` is used. @@ -389,7 +383,6 @@ However, you can supply any kind of custom `BlockingQueue` implementation. The following example shows how to do so for Logback: -==== [source, java] ---- public class MyEnhancedAppender extends AmqpAppender { @@ -401,11 +394,9 @@ public class MyEnhancedAppender extends AmqpAppender { } ---- -==== -The Log4j 2 appender supports using a https://logging.apache.org/log4j/2.x/manual/appenders.html#BlockingQueueFactory[`BlockingQueueFactory`], as the following example shows: +The Log4j 2 appender supports using a https://logging.apache.org/log4j/2.x/manual/appenders/delegating.html#BlockingQueueFactory[`BlockingQueueFactory`], as the following example shows: -==== [source, xml] ---- @@ -416,4 +407,3 @@ The Log4j 2 appender supports using a https://logging.apache.org/log4j/2.x/manua ---- -==== diff --git a/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc b/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc new file mode 100644 index 0000000000..ae3cbf1555 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc @@ -0,0 +1,272 @@ +[[amqp-client]] += RabbitMQ AMQP 1.0 Support + +Version 4.0 introduces `spring-rabbitmq-client` module for https://www.rabbitmq.com/client-libraries/amqp-client-libraries[AMQP 1.0] protocol support on RabbitMQ. + +This artifact is based on the {rabbitmq-github}/rabbitmq-amqp-java-client[com.rabbitmq.client:amqp-client] library and therefore can work only with RabbitMQ and its AMQP 1.0 protocol support. +It cannot be used for any arbitrary AMQP 1.0 broker. +For that purpose a https://qpid.apache.org/components/jms/index.html[JMS bridge] and respective {spring-framework-docs}/integration/jms.html[Spring JMS] integration is recommended so far. + +This dependency has to be added to the project to be able to interact with RabbitMQ AMQP 1.0 support: + +.maven +[source,xml,subs="+attributes"] +---- + + org.springframework.amqp + spring-rabbitmq-client + {project-version} + +---- + +.gradle +[source,groovy,subs="+attributes"] +---- +compile 'org.springframework.amqp:spring-rabbitmq-client:{project-version}' +---- + +The `spring-rabbit` (for AMQP 0.9.1 protocol) comes as a transitive dependency for reusing some common API in this new client, for example, exceptions, the `@RabbitListener` support. +It is not necessary to use both functionality in the target project, but RabbitMQ allows both AMQP 0.9.1 and 1.0 co-exists. + +For more information about RabbitMQ AMQP 1.0 Java Client see its https://www.rabbitmq.com/client-libraries/amqp-client-libraries[documentation]. + +[[amqp-client-environment]] +== RabbitMQ AMQP 1.0 Environment + +The `com.rabbitmq.client.amqp.Environment` is the first thing which has to be added to the project for connection management and other common settings. +It is an entry point to a node or a cluster of nodes. +The environment allows creating connections. +It can contain infrastructure-related configuration settings shared between connections, e.g. pools of threads, metrics and/or observation: + +[source,java] +---- +@Bean +Environment environment() { + return new AmqpEnvironmentBuilder() + .connectionSettings() + .port(5672) + .environmentBuilder() + .build(); +} +---- + +The same `Environment` instance can be used for connecting to different RabbitMQ brokers, then connection setting must be provided on specific connection. +See below. + +[[amqp-client-connection-factory]] +== AMQP Connection Factory + +The `org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory` abstraction was introduced to manage `com.rabbitmq.client.amqp.Connection`. +Don't confuse it with a `org.springframework.amqp.rabbit.connection.ConnectionFactory` which is only for AMQP 0.9.1 protocol. +The `SingleAmqpConnectionFactory` implementation is present to manage one connection and its settings. +The same `Connection` can be shared between many producers, consumers and management. +The multi-plexing is handled by the link abstraction for AMQP 1.0 protocol implementation internally in the AMQP client library. +The `Connection` has recovery capabilities and also handles topology. + +In most cases there is just enough to add this bean into the project: + +[source,java] +---- +@Bean +AmqpConnectionFactory connectionFactory(Environment environment) { + return new SingleAmqpConnectionFactory(environment); +} +---- + +See `SingleAmqpConnectionFactory` setters for all connection-specific setting. + +[[amqp-client-topology]] +== RabbitMQ Topology Management + +For topology management (exchanges, queues and binding between) from the application perspective, the `RabbitAmqpAdmin` is present, which is an implementation of existing `AmqpAdmin` interface: + +[source,java] +---- +@Bean +RabbitAmqpAdmin admin(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpAdmin(connectionFactory); +} +---- + +The same bean definitions for `Exchange`, `Queue`, `Binding` and `Declarables` instances as described in the xref:amqp/broker-configuration.adoc[] has to be used to manage topology. +The `RabbitAdmin` from `spring-rabbit` can also do that, but it happens against AMQP 0.9.1 connection, and since `RabbitAmqpAdmin` is based on the AMQP 1.0 connection, the topology recovery is handled smoothly from there, together with publishers and consumers recovery. + +The `RabbitAmqpAdmin` performs respective beans scanning in its `start()` lifecycle callback. +The `initialize()`, as well-as all other RabbitMQ entities management methods can be called manually at runtime. +Internally the `RabbitAmqpAdmin` uses `com.rabbitmq.client.amqp.Connection.management()` API to perform respective topology manipulations. + +[[amqp-client-template]] +== `RabbitAmqpTemplate` + +The `RabbitAmqpTemplate` is an implementation of the `AsyncAmqpTemplate` and performs various send/receive operations with AMQP 1.0 protocol. +Requires an `AmqpConnectionFactory` and can be configured with some defaults. +Even if `com.rabbitmq.client:amqp-client` library comes with a `com.rabbitmq.client.amqp.Message`, the `RabbitAmqpTemplate` still exposes an API based on the well-known `org.springframework.amqp.core.Message` with all the supporting classes like `MessageProperties` and `MessageConverter` abstraction. +The conversion to/from `com.rabbitmq.client.amqp.Message` is done internally in the `RabbitAmqpTemplate`. +All the methods return a `CompletableFuture` to obtain operation results eventually. +The operations with plain object require message body conversion and `SimpleMessageConverter` is used by default. +See xref:amqp/message-converters.adoc[] for more information about conversions. + +Usually, just one bean like this is enough to perform all the possible template pattern operation: + +[source,java] +---- +@Bean +RabbitAmqpTemplate rabbitTemplate(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpTemplate(connectionFactory); +} +---- + +It can be configured for some default exchange and routing key or just queue. +The `RabbitAmqpTemplate` have a default queue for receive operation and another default queue for request-reply operation where temporary queue is created for the request by the client if not present. + +Here are some samples of `RabbitAmqpTemplate` operations: + +[source,java] +---- +@Bean +DirectExchange e1() { + return new DirectExchange("e1"); +} + +@Bean +Queue q1() { + return QueueBuilder.durable("q1").deadLetterExchange("dlx1").build(); +} + +@Bean +Binding b1() { + return BindingBuilder.bind(q1()).to(e1()).with("k1"); +} + +... + +@Test +void defaultExchangeAndRoutingKey() { + this.rabbitAmqpTemplate.setExchange("e1"); + this.rabbitAmqpTemplate.setRoutingKey("k1"); + this.rabbitAmqpTemplate.setReceiveQueue("q1"); + + assertThat(this.rabbitAmqpTemplate.convertAndSend("test1")) + .succeedsWithin(Duration.ofSeconds(10)); + + assertThat(this.rabbitAmqpTemplate.receiveAndConvert()) + .succeedsWithin(Duration.ofSeconds(10)) + .isEqualTo("test1"); +} +---- + +Here we declared an `e1` exchange, `q1` queue and bind it into that exchange with a `k1` routing key. +Then we use a default setting for `RabbitAmqpTemplate` to publish messages to the mentioned exchange with the respective routing key and use `q1` as default queue for receiving operations. +There are overloaded variants for those methods to send to specific exchange or queue (for send and receive). +The `receiveAndConvert()` operations with a `ParameterizedTypeReference` requires a `SmartMessageConverter` to be injected into the `RabbitAmqpTemplate`. + +The next example demonstrate and RPC implementation with `RabbitAmqpTemplate` (assuming same RabbitMQ objects as in the previous example): + +[source,java] +---- +@Test +void verifyRpc() { + String testRequest = "rpc-request"; + String testReply = "rpc-reply"; + + CompletableFuture rpcClientResult = this.template.convertSendAndReceive("e1", "k1", testRequest); + + AtomicReference receivedRequest = new AtomicReference<>(); + CompletableFuture rpcServerResult = + this.rabbitAmqpTemplate.receiveAndReply("q1", + payload -> { + receivedRequest.set(payload); + return testReply; + }); + + assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(true); + assertThat(rpcClientResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(testReply); + assertThat(receivedRequest.get()).isEqualTo(testRequest); +} +---- + +The correlation and `replyTo` queue are managed internally. +The server side can be implemented with a `@RabbitListener` POJO method described below. + +[[amqp-client-listener]] +== The RabbitMQ AMQP 1.0 Consumer + +As with many other messaging implementations for consumer side, the `spring-rabbitmq-client` modules comes with the `RabbitAmqpListenerContainer` which is, essentially, an implementation of well-know `MessageListenerContainer`. +It does exactly the same as `DirectMessageListenerContainer`, but for RabbitMQ AMQP 1.0 support. +Requires an `AmqpConnectionFactory` and at least one queue to consume from. +Also, the `MessageListener` (or AMQP 1.0 specific `RabbitAmqpMessageListener`) must be provided. +Can be configured with an `autoSettle = false`, with the meaning of `AcknowledgeMode.MANUAL`. +In that case, the `Message` provided to the `MessageListener` has in its `MessageProperties` an `AmqpAcknowledgment` callback for target logic consideration. + +The `RabbitAmqpMessageListener` has a contract for `com.rabbitmq.client:amqp-client` abstractions: + +[source,java] +---- +/** + * Process an AMQP message. + * @param message the message to process. + * @param context the consumer context to settle message. + * Null if container is configured for {@code autoSettle}. + */ +void onAmqpMessage(Message message, Consumer.Context context); +---- + +Where the first argument is a native received `com.rabbitmq.client.amqp.Message` and `context` is a native callback for message settlement, similar to the mentioned above `AmqpAcknowledgment` abstraction. + +The `RabbitAmqpMessageListener` can handle and settle messages in batches when `batchSize` option is provided. +For this purpose the `MessageListener.onMessageBatch()` contract must be implemented. +The `batchReceiveDuration` option is used to schedule a force release for not full batches to avoid memory and https://www.rabbitmq.com/blog/2024/09/02/amqp-flow-control[consumer credits] exhausting. + +Usually, the `RabbitAmqpMessageListener` class is not used directly in the target project, and POJO method annotation configuration via `@RabbitListener` is chosen for declarative consumer configuration. +The `RabbitAmqpListenerContainerFactory` must be registered under the `RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME`, and `@RabbitListener` annotation process will register `RabbitAmqpMessageListener` instance into the `RabbitListenerEndpointRegistry`. +The target POJO method invocation is handled by specific `RabbitAmqpMessageListenerAdapter` implementation, which extends a `MessagingMessageListenerAdapter` and reuses a lot of its functionality, including request-reply scenarios (async or not). +So, all the concepts described in the xref:amqp/receiving-messages/async-annotation-driven.adoc[] are applied with this `RabbitAmqpMessageListener` as well. + +In addition to traditional messaging `payload` and `headers`, the `@RabbitListener` POJO method contract can be with these parameters: + +* `com.rabbitmq.client.amqp.Message` - the native AMQP 1.0 message without any conversions; +* `org.springframework.amqp.core.Message` - Spring AMQP message abstraction as conversion result from the native AMQP 1.0 message; +* `org.springframework.messaging.Message` - Spring Messaging abstraction as conversion result from the Spring AMQP message; +* `Consumer.Context` - RabbitMQ AMQP client consumer settlement API; +* `org.springframework.amqp.core.AmqpAcknowledgment` - Spring AMQP acknowledgment abstraction: delegates to the `Consumer.Context`. + +The following example demonstrates a simple `@RabbitListener` for RabbitMQ AMQP 1.0 interaction with the manual settlement: + +[source,java] +---- +@Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME) +RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpListenerContainerFactory(connectionFactory); +} + +final List received = Collections.synchronizedList(new ArrayList<>()); + +CountDownLatch consumeIsDone = new CountDownLatch(11); + +@RabbitListener(queues = {"q1", "q2"}, + ackMode = "#{T(org.springframework.amqp.core.AcknowledgeMode).MANUAL}", + concurrency = "2", + id = "testAmqpListener") +void processQ1AndQ2Data(String data, AmqpAcknowledgment acknowledgment, Consumer.Context context) { + try { + if ("discard".equals(data)) { + if (!this.received.contains(data)) { + context.discard(); + } + else { + throw new MessageConversionException("Test message is rejected"); + } + } + else if ("requeue".equals(data) && !this.received.contains(data)) { + acknowledgment.acknowledge(AmqpAcknowledgment.Status.REQUEUE); + } + else { + acknowledgment.acknowledge(); + } + this.received.add(data); + } + finally { + this.consumeIsDone.countDown(); + } +} +---- diff --git a/src/reference/antora/modules/ROOT/pages/reference.adoc b/src/reference/antora/modules/ROOT/pages/reference.adoc new file mode 100644 index 0000000000..247bbac91b --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/reference.adoc @@ -0,0 +1,9 @@ +[[reference]] += Reference + +This part of the reference documentation details the various components that comprise Spring AMQP. +The xref:amqp.adoc[main chapter] covers the core classes to develop an AMQP application. +This part also includes a chapter about the xref:sample-apps.adoc[sample applications]. + + + diff --git a/src/reference/antora/modules/ROOT/pages/resources.adoc b/src/reference/antora/modules/ROOT/pages/resources.adoc new file mode 100644 index 0000000000..f372038923 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/resources.adoc @@ -0,0 +1,6 @@ +[[resources]] += Other Resources + +In addition to this reference documentation, there exist a number of other resources that may help you learn about AMQP. + + diff --git a/src/reference/asciidoc/sample-apps.adoc b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc similarity index 93% rename from src/reference/asciidoc/sample-apps.adoc rename to src/reference/antora/modules/ROOT/pages/sample-apps.adoc index b75dc60181..842c5c72a2 100644 --- a/src/reference/asciidoc/sample-apps.adoc +++ b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc @@ -1,5 +1,5 @@ [[sample-apps]] -=== Sample Applications += Sample Applications The https://github.com/SpringSource/spring-amqp-samples[Spring AMQP Samples] project includes two sample applications. The first is a simple "`Hello World`" example that demonstrates both synchronous and asynchronous message reception. @@ -9,22 +9,21 @@ In this chapter, we provide a quick walk-through of each sample so that you can The samples are both Maven-based, so you should be able to import them directly into any Maven-aware IDE (such as https://www.springsource.org/sts[SpringSource Tool Suite]). [[hello-world-sample]] -==== The "`Hello World`" Sample +== The "`Hello World`" Sample The "`Hello World`" sample demonstrates both synchronous and asynchronous message reception. You can import the `spring-rabbit-helloworld` sample into the IDE and then follow the discussion below. [[hello-world-sync]] -===== Synchronous Example +=== Synchronous Example Within the `src/main/java` directory, navigate to the `org.springframework.amqp.helloworld` package. Open the `HelloWorldConfiguration` class and notice that it contains the `@Configuration` annotation at the class level and notice some `@Bean` annotations at method-level. This is an example of Spring's Java-based configuration. -You can read more about that https://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-java[here]. +You can read more about that {spring-framework-docs}/core/beans/java.html[here]. The following listing shows how the connection factory is created: -==== [source,java] ---- @Bean @@ -36,14 +35,12 @@ public CachingConnectionFactory connectionFactory() { return connectionFactory; } ---- -==== The configuration also contains an instance of `RabbitAdmin`, which, by default, looks for any beans of type exchange, queue, or binding and then declares them on the broker. In fact, the `helloWorldQueue` bean that is generated in `HelloWorldConfiguration` is an example because it is an instance of `Queue`. The following listing shows the `helloWorldQueue` bean definition: -==== [source,java] ---- @Bean @@ -51,7 +48,6 @@ public Queue helloWorldQueue() { return new Queue(this.helloWorldQueueName); } ---- -==== Looking back at the `rabbitTemplate` bean configuration, you can see that it has the name of `helloWorldQueue` set as its `queue` property (for receiving messages) and for its `routingKey` property (for sending messages). @@ -61,7 +57,6 @@ It contains a `main()` method where the Spring `ApplicationContext` is created. The following listing shows the `main` method: -==== [source,java] ---- public static void main(String[] args) { @@ -72,7 +67,6 @@ public static void main(String[] args) { System.out.println("Sent: Hello World"); } ---- -==== In the preceding example, the `AmqpTemplate` bean is retrieved and used for sending a `Message`. Since the client code should rely on interfaces whenever possible, the type is `AmqpTemplate` rather than `RabbitTemplate`. @@ -83,12 +77,11 @@ In this case, it uses the default `SimpleMessageConverter`, but a different impl Now open the `Consumer` class. It actually shares the same configuration base class, which means it shares the `rabbitTemplate` bean. That is why we configured that template with both a `routingKey` (for sending) and a `queue` (for receiving). -As we describe in <>, you could instead pass the 'routingKey' argument to the send method and the 'queue' argument to the receive method. +As we describe in xref:amqp/template.adoc[`AmqpTemplate`], you could instead pass the 'routingKey' argument to the send method and the 'queue' argument to the receive method. The `Consumer` code is basically a mirror image of the Producer, calling `receiveAndConvert()` rather than `convertAndSend()`. The following listing shows the main method for the `Consumer`: -==== [source,java] ---- public static void main(String[] args) { @@ -98,14 +91,13 @@ public static void main(String[] args) { System.out.println("Received: " + amqpTemplate.receiveAndConvert()); } ---- -==== If you run the `Producer` and then run the `Consumer`, you should see `Received: Hello World` in the console output. [[hello-world-async]] -===== Asynchronous Example +=== Asynchronous Example -<> walked through the synchronous Hello World sample. +xref:sample-apps.adoc#hello-world-sync[Synchronous Example] walked through the synchronous Hello World sample. This section describes a slightly more advanced but significantly more powerful option. With a few modifications, the Hello World sample can provide an example of asynchronous reception, also known as message-driven POJOs. In fact, there is a sub-package that provides exactly that: `org.springframework.amqp.samples.helloworld.async`. @@ -120,7 +112,6 @@ That is why we only need to provide the routing key here. The following listing shows the `rabbitTemplate` definition: -==== [source,java] ---- public RabbitTemplate rabbitTemplate() { @@ -129,7 +120,6 @@ public RabbitTemplate rabbitTemplate() { return template; } ---- -==== Since this sample demonstrates asynchronous message reception, the producing side is designed to continuously send messages (if it were a message-per-execution model like the synchronous version, it would not be quite so obvious that it is, in fact, a message-driven consumer). The component responsible for continuously sending messages is defined as an inner class within the `ProducerConfiguration`. @@ -137,7 +127,6 @@ It is configured to run every three seconds. The following listing shows the component: -==== [source,java] ---- static class ScheduledProducer { @@ -153,17 +142,15 @@ static class ScheduledProducer { } } ---- -==== You do not need to understand all of the details, since the real focus should be on the receiving side (which we cover next). -However, if you are not yet familiar with Spring task scheduling support, you can learn more https://docs.spring.io/spring/docs/current/spring-framework-reference/html/scheduling.html#scheduling-annotation-support[here]. +However, if you are not yet familiar with Spring task scheduling support, you can learn more {spring-framework-docs}/integration/scheduling.html#scheduling-annotation-support-scheduled[here]. The short story is that the `postProcessor` bean in the `ProducerConfiguration` registers the task with a scheduler. Now we can turn to the receiving side. To emphasize the message-driven POJO behavior, we start with the component that react to the messages. The class is called `HelloWorldHandler` and is shown in the following listing: -==== [source,java] ---- public class HelloWorldHandler { @@ -174,7 +161,6 @@ public class HelloWorldHandler { } ---- -==== That class is a POJO. It does not extend any base class, it does not implement any interfaces, and it does not even contain any imports. @@ -185,7 +171,6 @@ You can see the POJO wrapped in the adapter there. The following listing shows how the `listenerContainer` is defined: -==== [source,java] ---- @Bean @@ -197,16 +182,16 @@ public SimpleMessageListenerContainer listenerContainer() { return container; } ---- -==== The `SimpleMessageListenerContainer` is a Spring lifecycle component and, by default, starts automatically. If you look in the `Consumer` class, you can see that its `main()` method consists of nothing more than a one-line bootstrap to create the `ApplicationContext`. The Producer's `main()` method is also a one-line bootstrap, since the component whose method is annotated with `@Scheduled` also starts automatically. You can start the `Producer` and `Consumer` in any order, and you should see messages being sent and received every three seconds. -==== Stock Trading +[[stock-trading]] +== Stock Trading -The Stock Trading sample demonstrates more advanced messaging scenarios than <>. +The Stock Trading sample demonstrates more advanced messaging scenarios than xref:sample-apps.adoc#hello-world-sample[the Hello World sample]. However, the configuration is very similar, if a bit more involved. Since we walked through the Hello World configuration in detail, here, we focus on what makes this sample different. There is a server that pushes market data (stock quotations) to a topic exchange. @@ -225,21 +210,18 @@ First, it configures the market data exchange on the `RabbitTemplate` so that it It does this within an abstract callback method defined in the base configuration class. The following listing shows that method: -==== [source,java] ---- public void configureRabbitTemplate(RabbitTemplate rabbitTemplate) { rabbitTemplate.setExchange(MARKET_DATA_EXCHANGE_NAME); } ---- -==== Second, the stock request queue is declared. It does not require any explicit bindings in this case, because it is bound to the default no-name exchange with its own name as the routing key. As mentioned earlier, the AMQP specification defines that behavior. The following listing shows the definition of the `stockRequestQueue` bean: -==== [source,java] ---- @Bean @@ -247,7 +229,6 @@ public Queue stockRequestQueue() { return new Queue(STOCK_REQUEST_QUEUE_NAME); } ---- -==== Now that you have seen the configuration of the server's AMQP resources, navigate to the `org.springframework.amqp.rabbit.stocks` package under the `src/test/java` directory. There, you can see the actual `Server` class that provides a `main()` method. @@ -263,20 +244,17 @@ Notice that it is not itself coupled to the framework or any of the AMQP concept It accepts a `TradeRequest` and returns a `TradeResponse`. The following listing shows the definition of the `handleMessage` method: -==== [source,java] ---- public TradeResponse handleMessage(TradeRequest tradeRequest) { ... } ---- -==== Now that we have seen the most important configuration and code for the server, we can turn to the client. The best starting point is probably `RabbitClientConfiguration`, in the `org.springframework.amqp.rabbit.stocks.config.client` package. Notice that it declares two queues without providing explicit names. The following listing shows the bean definitions for the two queues: -==== [source,java] ---- @Bean @@ -289,7 +267,6 @@ public Queue traderJoeQueue() { return amqpAdmin().declareQueue(); } ---- -==== Those are private queues, and unique names are generated automatically. The first generated queue is used by the client to bind to the market data exchange that has been exposed by the server. @@ -299,7 +276,6 @@ Since the market data exchange is a topic exchange, the binding can be expressed The `RabbitClientConfiguration` does so with a `Binding` object, and that object is generated with the `BindingBuilder` fluent API. The following listing shows the `Binding`: -==== [source,java] ---- @Value("${stocks.quote.pattern}") @@ -311,7 +287,6 @@ public Binding marketDataBinding() { marketDataQueue()).to(marketDataExchange()).with(marketDataRoutingKey); } ---- -==== Notice that the actual value has been externalized in a properties file (`client.properties` under `src/main/resources`), and that we use Spring's `@Value` annotation to inject that value. This is generally a good idea. @@ -331,7 +306,6 @@ The corresponding code on the `Client` side is `RabbitStockServiceGateway` in th It delegates to the `RabbitTemplate` in order to send messages. The following listing shows the `send` method: -==== [source,java] ---- public void send(TradeRequest tradeRequest) { @@ -350,13 +324,11 @@ public void send(TradeRequest tradeRequest) { }); } ---- -==== Notice that, prior to sending the message, it sets the `replyTo` address. It provides the queue that was generated by the `traderJoeQueue` bean definition (shown earlier). The following listing shows the `@Bean` definition for the `StockServiceGateway` class itself: -==== [source,java] ---- @Bean @@ -367,17 +339,16 @@ public StockServiceGateway stockServiceGateway() { return gateway; } ---- -==== If you are no longer running the server and client, start them now. Try sending a request with the format of '100 TCKR'. After a brief artificial delay that simulates "`processing`" of the request, you should see a confirmation message appear on the client. [[spring-rabbit-json]] -==== Receiving JSON from Non-Spring Applications +== Receiving JSON from Non-Spring Applications Spring applications, when sending JSON, set the `__TypeId__` header to the fully qualified class name to assist the receiving application in converting the JSON back to a Java object. The `spring-rabbit-json` sample explores several techniques to convert the JSON from a non-Spring application. -See also <> as well as the https://docs.spring.io/spring-amqp/docs/current/api/index.html?org/springframework/amqp/support/converter/DefaultClassMapper.html[Javadoc for the `DefaultClassMapper`]. +See also xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] as well as the javadoc:org.springframework.amqp.support.converter.DefaultClassMapper[Javadoc for the `DefaultClassMapper`]. diff --git a/src/reference/antora/modules/ROOT/pages/stream.adoc b/src/reference/antora/modules/ROOT/pages/stream.adoc new file mode 100644 index 0000000000..b1e35babbf --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/stream.adoc @@ -0,0 +1,307 @@ +[[stream-support]] += Using the RabbitMQ Stream Plugin + +Version 2.4 introduces initial support for the {rabbitmq-github}/rabbitmq-stream-java-client[RabbitMQ Stream Plugin Java Client] for the https://rabbitmq.com/stream.html[RabbitMQ Stream Plugin]. + +* `RabbitStreamTemplate` +* `StreamListenerContainer` + +Add the `spring-rabbit-stream` dependency to your project: + +.maven +[source,xml,subs="+attributes"] +---- + + org.springframework.amqp + spring-rabbit-stream + {project-version} + +---- + +.gradle +[source,groovy,subs="+attributes"] +---- +compile 'org.springframework.amqp:spring-rabbit-stream:{project-version}' +---- + +You can provision the queues as normal, using a `RabbitAdmin` bean, using the `QueueBuilder.stream()` method to designate the queue type. +For example: + +[source, java] +---- +@Bean +Queue stream() { + return QueueBuilder.durable("stream.queue1") + .stream() + .build(); +} +---- + +However, this will only work if you are also using non-stream components (such as the `SimpleMessageListenerContainer` or `DirectMessageListenerContainer`) because the admin is triggered to declare the defined beans when an AMQP connection is opened. +If your application only uses stream components, or you wish to use advanced stream configuration features, you should configure a `StreamAdmin` instead: + +[source, java] +---- +@Bean +StreamAdmin streamAdmin(Environment env) { + return new StreamAdmin(env, sc -> { + sc.stream("stream.queue1").maxAge(Duration.ofHours(2)).create(); + sc.stream("stream.queue2").create(); + }); +} +---- + +Refer to the RabbitMQ documentation for more information about the `StreamCreator`. + +[[sending-messages]] +== Sending Messages + +The `RabbitStreamTemplate` provides a subset of the `RabbitTemplate` (AMQP) functionality. + +.RabbitStreamOperations +[source, java] +---- +public interface RabbitStreamOperations extends AutoCloseable { + + CompletableFuture send(Message message); + + CompletableFuture convertAndSend(Object message); + + CompletableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); + + CompletableFuture send(com.rabbitmq.stream.Message message); + + MessageBuilder messageBuilder(); + + MessageConverter messageConverter(); + + StreamMessageConverter streamMessageConverter(); + + @Override + void close() throws AmqpException; + +} +---- + +The `RabbitStreamTemplate` implementation has the following constructor and properties: + +.RabbitStreamTemplate +[source, java] +---- +public RabbitStreamTemplate(Environment environment, String streamName) { +} + +public void setMessageConverter(MessageConverter messageConverter) { +} + +public void setStreamConverter(StreamMessageConverter streamConverter) { +} + +public synchronized void setProducerCustomizer(ProducerCustomizer producerCustomizer) { +} +---- + +The `MessageConverter` is used in the `convertAndSend` methods to convert the object to a Spring AMQP `Message`. + +The `StreamMessageConverter` is used to convert from a Spring AMQP `Message` to a native stream `Message`. + +You can also send native stream `Message` s directly; with the `messageBuilder()` method providing access to the `Producer` 's message builder. + +The `ProducerCustomizer` provides a mechanism to customize the producer before it is built. + +Refer to the {rabbitmq-stream-docs}[Java Client Documentation] about customizing the `Environment` and `Producer`. + +[[receiving-messages]] +== Receiving Messages + +Asynchronous message reception is provided by the `StreamListenerContainer` (and the `StreamRabbitListenerContainerFactory` when using `@RabbitListener`). + +The listener container requires an `Environment` as well as a single stream name. + +You can either receive Spring AMQP `Message` s using the classic `MessageListener`, or you can receive native stream `Message` s using a new interface: + +[source, java] +---- +public interface StreamMessageListener extends MessageListener { + + void onStreamMessage(Message message, Context context); + +} +---- + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for information about supported properties. + +Similar the template, the container has a `ConsumerCustomizer` property. + +Refer to the {rabbitmq-stream-docs}[Java Client Documentation] about customizing the `Environment` and `Consumer`. + +When using `@RabbitListener`, configure a `StreamRabbitListenerContainerFactory`; at this time, most `@RabbitListener` properties (`concurrency`, etc) are ignored. Only `id`, `queues`, `autoStartup` and `containerFactory` are supported. +In addition, `queues` can only contain one stream name. + +[[stream-examples]] +== Examples + +[source, java] +---- +@Bean +RabbitStreamTemplate streamTemplate(Environment env) { + RabbitStreamTemplate template = new RabbitStreamTemplate(env, "test.stream.queue1"); + template.setProducerCustomizer((name, builder) -> builder.name("test")); + return template; +} + +@Bean +RabbitListenerContainerFactory rabbitListenerContainerFactory(Environment env) { + return new StreamRabbitListenerContainerFactory(env); +} + +@RabbitListener(queues = "test.stream.queue1") +void listen(String in) { + ... +} + +@Bean +RabbitListenerContainerFactory nativeFactory(Environment env) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setNativeListener(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name("myConsumer") + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; +} + +@RabbitListener(id = "test", queues = "test.stream.queue2", containerFactory = "nativeFactory") +void nativeMsg(Message in, Context context) { + ... + context.storeOffset(); +} + +@Bean +Queue stream() { + return QueueBuilder.durable("test.stream.queue1") + .stream() + .build(); +} + +@Bean +Queue stream() { + return QueueBuilder.durable("test.stream.queue2") + .stream() + .build(); +} +---- + +Version 2.4.5 added the `adviceChain` property to the `StreamListenerContainer` (and its factory). +A new factory bean is also provided to create a stateless retry interceptor with an optional `StreamMessageRecoverer` for use when consuming raw stream messages. + +[source, java] +---- +@Bean +public StreamRetryOperationsInterceptorFactoryBean sfb(RetryTemplate retryTemplate) { + StreamRetryOperationsInterceptorFactoryBean rfb = + new StreamRetryOperationsInterceptorFactoryBean(); + rfb.setRetryOperations(retryTemplate); + rfb.setStreamMessageRecoverer((msg, context, throwable) -> { + ... + }); + return rfb; +} +---- + +IMPORTANT: Stateful retry is not supported with this container. + +[[super-streams]] +== Super Streams + +A Super Stream is an abstract concept for a partitioned stream, implemented by binding a number of stream queues to an exchange having an argument `x-super-stream: true`. + +[[provisioning]] +=== Provisioning + +For convenience, a super stream can be provisioned by defining a single bean of type `SuperStream`. + +[source, java] +---- +@Bean +SuperStream superStream() { + return new SuperStream("my.super.stream", 3); +} +---- + +The `RabbitAdmin` detects this bean and will declare the exchange (`my.super.stream`) and 3 queues (partitions) - `my.super-stream-n` where `n` is `0`, `1`, `2`, bound with routing keys equal to `n`. + +If you also wish to publish over AMQP to the exchange, you can provide custom routing keys: + +[source, java] +---- +@Bean +SuperStream superStream() { + return new SuperStream("my.super.stream", 3, (q, i) -> IntStream.range(0, i) + .mapToObj(j -> "rk-" + j) + .collect(Collectors.toList())); +} +---- + +The number of keys must equal the number of partitions. + +[[producing-to-a-superstream]] +=== Producing to a SuperStream + +You must add a `superStreamRoutingFunction` to the `RabbitStreamTemplate`: + +[source, java] +---- +@Bean +RabbitStreamTemplate streamTemplate(Environment env) { + RabbitStreamTemplate template = new RabbitStreamTemplate(env, "stream.queue1"); + template.setSuperStreamRouting(message -> { + // some logic to return a String for the client's hashing algorithm + }); + return template; +} +---- + +You can also publish over AMQP, using the `RabbitTemplate`. + +[[super-stream-consumer]] +=== Consuming Super Streams with Single Active Consumers + +Invoke the `superStream` method on the listener container to enable a single active consumer on a super stream. + +[source, java] +---- +@Bean +StreamListenerContainer container(Environment env, String name) { + StreamListenerContainer container = new StreamListenerContainer(env); + container.superStream("ss.sac", "myConsumer", 3); // concurrency = 3 + container.setupMessageListener(msg -> { + ... + }); + container.setConsumerCustomizer((id, builder) -> builder.offset(OffsetSpecification.last())); + return container; +} +---- + +IMPORTANT: At this time, when the concurrency is greater than 1, the actual concurrency is further controlled by the `Environment`; to achieve full concurrency, set the environment's `maxConsumersByConnection` to 1. +See {rabbitmq-stream-docs}/#configuring-the-environment[Configuring the Environment]. + +[[stream-micrometer-observation]] +== Micrometer Observation + +Using Micrometer for observation is now supported, since version 3.0.5, for the `RabbitStreamTemplate` and the stream listener container. +The container now also supports Micrometer timers (when observation is not enabled). + +Set `observationEnabled` on each component to enable observation; this will disable xref:amqp/receiving-messages/micrometer.adoc[Micrometer Timers] because the timers will now be managed with each observation. +When using annotated listeners, set `observationEnabled` on the container factory. + +Refer to {micrometer-tracing-docs}[Micrometer Tracing] for more information. + +To add tags to timers/traces, configure a custom `RabbitStreamTemplateObservationConvention` or `RabbitStreamListenerObservationConvention` to the template or listener container, respectively. + +The default implementations add the `name` tag for template observations and `listener.id` tag for containers. + +You can either subclass `DefaultRabbitStreamTemplateObservationConvention` or `DefaultStreamRabbitListenerObservationConvention` or provide completely new implementations. + +See xref:appendix/micrometer.adoc[Micrometer Observation Documentation] for more details. diff --git a/src/reference/asciidoc/testing.adoc b/src/reference/antora/modules/ROOT/pages/testing.adoc similarity index 94% rename from src/reference/asciidoc/testing.adoc rename to src/reference/antora/modules/ROOT/pages/testing.adoc index 5a3c314fc6..42571eb1a5 100644 --- a/src/reference/asciidoc/testing.adoc +++ b/src/reference/antora/modules/ROOT/pages/testing.adoc @@ -1,5 +1,5 @@ [[testing]] -=== Testing Support += Testing Support Writing integration for asynchronous applications is necessarily more complex than testing simpler applications. This is made more complex when abstractions such as the `@RabbitListener` annotations come into the picture. @@ -14,14 +14,14 @@ It is anticipated that this project will expand over time, but we need community Please use https://jira.spring.io/browse/AMQP[JIRA] or https://github.com/spring-projects/spring-amqp/issues[GitHub Issues] to provide such feedback. [[spring-rabbit-test]] -==== @SpringRabbitTest +== @SpringRabbitTest Use this annotation to add infrastructure beans to the Spring test `ApplicationContext`. This is not necessary when using, for example `@SpringBootTest` since Spring Boot's auto configuration will add the beans. Beans that are registered are: -* `CachingConnectionFactory` (`autoConnectionFactory`). If `@RabbitEnabled` is present, its connectionn factory is used. +* `CachingConnectionFactory` (`autoConnectionFactory`). If `@RabbitEnabled` is present, its connection factory is used. * `RabbitTemplate` (`autoRabbitTemplate`) * `RabbitAdmin` (`autoRabbitAdmin`) * `RabbitListenerContainerFactory` (`autoContainerFactory`) @@ -29,10 +29,9 @@ Beans that are registered are: In addition, the beans associated with `@EnableRabbit` (to support `@RabbitListener`) are added. .Junit5 example -==== [source, java] ---- -@SpringJunitConfig +@SpringJUnitConfig @SpringRabbitTest public class MyRabbitTests { @@ -59,19 +58,17 @@ public class MyRabbitTests { } ---- -==== -With JUnit4, replace `@SpringJunitConfig` with `@RunWith(SpringRunnner.class)`. +With JUnit4, replace `@SpringJUnitConfig` with `@RunWith(SpringRunnner.class)`. [[mockito-answer]] -==== Mockito `Answer` Implementations +== Mockito `Answer` Implementations There are currently two `Answer` implementations to help with testing. The first, `LatchCountDownAndCallRealMethodAnswer`, provides an `Answer` that returns `null` and counts down a latch. The following example shows how to use `LatchCountDownAndCallRealMethodAnswer`: -==== [source, java] ---- LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("myListener", 2); @@ -82,14 +79,12 @@ doAnswer(answer) assertThat(answer.await(10)).isTrue(); ---- -==== The second, `LambdaAnswer` provides a mechanism to optionally call the real method and provides an opportunity to return a custom result, based on the `InvocationOnMock` and the result (if any). Consider the following POJO: -==== [source, java] ---- public class Thing { @@ -100,11 +95,9 @@ public class Thing { } ---- -==== The following class tests the `Thing` POJO: -==== [source, java] ---- Thing thing = spy(new Thing()); @@ -121,15 +114,14 @@ doAnswer(new LambdaAnswer(false, (i, r) -> "" + i.getArguments()[0] + i.getArguments()[0])).when(thing).thing(anyString()); assertEquals("thingthing", thing.thing("thing")); ---- -==== Starting with version 2.2.3, the answers capture any exceptions thrown by the method under test. Use `answer.getExceptions()` to get a reference to them. -When used in conjunction with the <> use `harness.getLambdaAnswerFor("listenerId", true, ...)` to get a properly constructed answer for the listener. +When used in conjunction with the xref:testing.adoc#test-harness[`@RabbitListenerTest` and `RabbitListenerTestHarness`] use `harness.getLambdaAnswerFor("listenerId", true, ...)` to get a properly constructed answer for the listener. [[test-harness]] -==== `@RabbitListenerTest` and `RabbitListenerTestHarness` +== `@RabbitListenerTest` and `RabbitListenerTestHarness` Annotating one of your `@Configuration` classes with `@RabbitListenerTest` causes the framework to replace the standard `RabbitListenerAnnotationBeanPostProcessor` with a subclass called `RabbitListenerTestHarness` (it also enables @@ -149,7 +141,6 @@ Consider some examples. The following example uses spy: -==== [source, java] ---- @Configuration @@ -224,11 +215,9 @@ We use one of the link:#mockito-answer[Answer] implementations to help with t IMPORTANT: Due to the way the listener is spied, it is important to use `harness.getLatchAnswerFor()` to get a properly configured answer for the spy. <4> Configure the spy to invoke the `Answer`. -==== The following example uses the capture advice: -==== [source, java] ---- @Configuration @@ -311,13 +300,12 @@ for the result. to suspend the test thread. <5> When the listener throws an exception, it is available in the `throwable` property of the invocation data. -==== IMPORTANT: When using custom `Answer` s with the harness, in order to operate properly, such answers should subclass `ForwardsInvocation` and get the actual listener (not the spy) from the harness (`getDelegate("myListener")`) and call `super.answer(invocation)`. -See the provided <> source code for examples. +See the provided xref:testing.adoc#mockito-answer[Mockito `Answer` Implementations] source code for examples. [[test-template]] -==== Using `TestRabbitTemplate` +== Using `TestRabbitTemplate` The `TestRabbitTemplate` is provided to perform some basic integration testing without the need for a broker. When you add it as a `@Bean` in your test case, it discovers all the listener containers in the context, whether declared as `@Bean` or `` or using the `@RabbitListener` annotation. @@ -327,7 +315,6 @@ Request-reply messaging (`sendAndReceive` methods) is supported for listeners th The following test case uses the template: -==== [source, java] ---- @RunWith(SpringRunner.class) @@ -435,16 +422,16 @@ public class TestRabbitTemplateTests { } ---- -==== [[junit-rules]] -==== JUnit4 `@Rules` +== JUnit4 `@Rules` Spring AMQP version 1.7 and later provide an additional jar called `spring-rabbit-junit`. This jar contains a couple of utility `@Rule` instances for use when running JUnit4 tests. -See <> for JUnit5 testing. +See xref:testing.adoc#junit5-conditions[JUnit5 Conditions] for JUnit5 testing. -===== Using `BrokerRunning` +[[using-brokerrunning]] +=== Using `BrokerRunning` `BrokerRunning` provides a mechanism to let tests succeed when a broker is not running (on `localhost`, by default). @@ -452,7 +439,6 @@ It also has utility methods to initialize and empty queues and delete queues and The following example shows its usage: -==== [source, java] ---- @@ -464,12 +450,11 @@ public static void tearDown() { brokerRunning.removeTestQueues("some.other.queue.too") // removes foo, bar as well } ---- -==== There are several `isRunning...` static methods, such as `isBrokerAndManagementRunning()`, which verifies the broker has the management plugin enabled. [[brokerRunning-configure]] -====== Configuring the Rule +==== Configuring the Rule There are times when you want tests to fail if there is no broker, such as a nightly CI build. To disable the rule at runtime, set an environment variable called `RABBITMQ_SERVER_REQUIRED` to `true`. @@ -478,7 +463,6 @@ You can override the broker properties, such as hostname with either setters or The following example shows how to override properties with setters: -==== [source, java] ---- @@ -494,11 +478,9 @@ public static void tearDown() { brokerRunning.removeTestQueues("some.other.queue.too") // removes foo, bar as well } ---- -==== You can also override properties by setting the following environment variables: -==== [source, java] ---- public static final String BROKER_ADMIN_URI = "RABBITMQ_TEST_ADMIN_URI"; @@ -509,7 +491,6 @@ public static final String BROKER_PW = "RABBITMQ_TEST_PASSWORD"; public static final String BROKER_ADMIN_USER = "RABBITMQ_TEST_ADMIN_USER"; public static final String BROKER_ADMIN_PW = "RABBITMQ_TEST_ADMIN_PASSWORD"; ---- -==== These environment variables override the default settings (`localhost:5672` for amqp and `http://localhost:15672/api/` for the management REST API). @@ -525,7 +506,6 @@ Invoke `clearEnvironmentVariableOverrides()` to reset the rule to use defaults ( In your test cases, you can use the `brokerRunning` when creating the connection factory; `getConnectionFactory()` returns the rule's RabbitMQ `ConnectionFactory`. The following example shows how to do so: -==== [source, java] ---- @Bean @@ -533,31 +513,30 @@ public CachingConnectionFactory rabbitConnectionFactory() { return new CachingConnectionFactory(brokerRunning.getConnectionFactory()); } ---- -==== -===== Using `LongRunningIntegrationTest` +[[using-longrunningintegrationtest]] +=== Using `LongRunningIntegrationTest` `LongRunningIntegrationTest` is a rule that disables long running tests. You might want to use this on a developer system but ensure that the rule is disabled on, for example, nightly CI builds. The following example shows its usage: -==== [source, java] ---- @Rule public LongRunningIntegrationTest longTests = new LongRunningIntegrationTest(); ---- -==== To disable the rule at runtime, set an environment variable called `RUN_LONG_INTEGRATION_TESTS` to `true`. [[junit5-conditions]] -==== JUnit5 Conditions +== JUnit5 Conditions Version 2.0.2 introduced support for JUnit5. -===== Using the `@RabbitAvailable` Annotation +[[using-the-rabbitavailable-annotation]] +=== Using the `@RabbitAvailable` Annotation This class-level annotation is similar to the `BrokerRunning` `@Rule` discussed in <>. It is processed by the `RabbitAvailableCondition`. @@ -569,8 +548,8 @@ The annotation has three properties: * `purgeAfterEach`: (Since version 2.2) when `true` (default), the `queues` will be purged between tests. It is used to check whether the broker is available and skip the tests if not. -As discussed in <>, the environment variable called `RABBITMQ_SERVER_REQUIRED`, if `true`, causes the tests to fail fast if there is no broker. -You can configure the condition by using environment variables as discussed in <>. +As discussed in xref:testing.adoc#brokerRunning-configure[Configuring the Rule], the environment variable called `RABBITMQ_SERVER_REQUIRED`, if `true`, causes the tests to fail fast if there is no broker. +You can configure the condition by using environment variables as discussed in xref:testing.adoc#brokerRunning-configure[Configuring the Rule]. In addition, the `RabbitAvailableCondition` supports argument resolution for parameterized test constructors and methods. Two argument types are supported: @@ -580,7 +559,6 @@ Two argument types are supported: The following example shows both: -==== [source, java] ---- @RabbitAvailable(queues = "rabbitAvailableTests.queue") @@ -605,13 +583,11 @@ public class RabbitAvailableCTORInjectionTests { } ---- -==== The preceding test is in the framework itself and verifies the argument injection and that the condition created the queue properly. A practical user test might be as follows: -==== [source, java] ---- @RabbitAvailable(queues = "rabbitAvailableTests.queue") @@ -631,7 +607,6 @@ public class RabbitAvailableCTORInjectionTests { } } ---- -==== When you use a Spring annotation application context within a test class, you can get a reference to the condition's connection factory through a static method called `RabbitAvailableCondition.getBrokerRunning()`. @@ -640,7 +615,6 @@ The new class has the same API as `BrokerRunning`. The following test comes from the framework and demonstrates the usage: -==== [source, java] ---- @RabbitAvailable(queues = { @@ -702,14 +676,13 @@ public class RabbitTemplateMPPIntegrationTests { } ---- -==== -===== Using the `@LongRunning` Annotation +[[using-the-longrunning-annotation]] +=== Using the `@LongRunning` Annotation Similar to the `LongRunningIntegrationTest` JUnit4 `@Rule`, this annotation causes tests to be skipped unless an environment variable (or system property) is set to `true`. The following example shows how to use it: -==== [source, java] ---- @RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE) @@ -722,6 +695,5 @@ public class SimpleMessageListenerContainerLongTests { } ---- -==== By default, the variable is `RUN_LONG_INTEGRATION_TESTS`, but you can specify the variable name in the annotation's `value` attribute. diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc new file mode 100644 index 0000000000..3031c1cb17 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -0,0 +1,25 @@ +[[whats-new]] += What's New +:page-section-summary-toc: 1 + +[[changes-in-4-2-since-3-2]] +== Changes in 4.0 Since 3.2 + +[[spring-framework-7-0]] +=== Spring Framework 7.0 + +This version requires Spring Framework 7.0. + +[[x40-null-away]] +=== Null-safety + +As many other Spring portfolio projects, Spring AMQP has been migrated to https://jspecify.dev/docs/start-here[JSpecify] annotations to declare the nullness of API. +The https://github.com/uber/NullAway[NullAway] Gradle plugin is used to check the consistency of null-safety declarations. + +[[x40-rabbitmq-amqp-client]] +=== The `spring-rabbitmq-client` module + +The new `spring-rabbitmq-client` module (with same artifact name) is introduced. +This is an implementation of AMQP 1.0 protocol specific to RabbitMQ since `4.0` and based on the `com.rabbitmq.client:amqp-client` library. + +See xref:rabbitmq-amqp-client.adoc[] for more information. \ No newline at end of file diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc deleted file mode 100644 index 2e127824fb..0000000000 --- a/src/reference/asciidoc/amqp.adoc +++ /dev/null @@ -1,6792 +0,0 @@ -[[amqp]] -=== Using Spring AMQP - -This chapter explores the interfaces and classes that are the essential components for developing applications with Spring AMQP. - -==== AMQP Abstractions - -Spring AMQP consists of two modules (each represented by a JAR in the distribution): `spring-amqp` and `spring-rabbit`. -The 'spring-amqp' module contains the `org.springframework.amqp.core` package. -Within that package, you can find the classes that represent the core AMQP "`model`". -Our intention is to provide generic abstractions that do not rely on any particular AMQP broker implementation or client library. -End user code can be more portable across vendor implementations as it can be developed against the abstraction layer only. -These abstractions are then implemented by broker-specific modules, such as 'spring-rabbit'. -There is currently only a RabbitMQ implementation. -However, the abstractions have been validated in .NET using Apache Qpid in addition to RabbitMQ. -Since AMQP operates at the protocol level, in principle, you can use the RabbitMQ client with any broker that supports the same protocol version, but we do not test any other brokers at present. - -This overview assumes that you are already familiar with the basics of the AMQP specification. -If not, have a look at the resources listed in <> - -===== `Message` - -The 0-9-1 AMQP specification does not define a `Message` class or interface. -Instead, when performing an operation such as `basicPublish()`, the content is passed as a byte-array argument and additional properties are passed in as separate arguments. -Spring AMQP defines a `Message` class as part of a more general AMQP domain model representation. -The purpose of the `Message` class is to encapsulate the body and properties within a single instance so that the API can, in turn, be simpler. -The following example shows the `Message` class definition: - -==== -[source,java] ----- -public class Message { - - private final MessageProperties messageProperties; - - private final byte[] body; - - public Message(byte[] body, MessageProperties messageProperties) { - this.body = body; - this.messageProperties = messageProperties; - } - - public byte[] getBody() { - return this.body; - } - - public MessageProperties getMessageProperties() { - return this.messageProperties; - } -} ----- -==== - -The `MessageProperties` interface defines several common properties, such as 'messageId', 'timestamp', 'contentType', and several more. -You can also extend those properties with user-defined 'headers' by calling the `setHeader(String key, Object value)` method. - -IMPORTANT: Starting with versions `1.5.7`, `1.6.11`, `1.7.4`, and `2.0.0`, if a message body is a serialized `Serializable` java object, it is no longer deserialized (by default) when performing `toString()` operations (such as in log messages). -This is to prevent unsafe deserialization. -By default, only `java.util` and `java.lang` classes are deserialized. -To revert to the previous behavior, you can add allowable class/package patterns by invoking `Message.addAllowedListPatterns(...)`. -A simple `*` wildcard is supported, for example `com.something.*, *.MyClass`. -Bodies that cannot be deserialized are represented by `byte[]` in log messages. - -===== Exchange - -The `Exchange` interface represents an AMQP Exchange, which is what a Message Producer sends to. -Each Exchange within a virtual host of a broker has a unique name as well as a few other properties. -The following example shows the `Exchange` interface: - -[source,java] ----- -public interface Exchange { - - String getName(); - - String getExchangeType(); - - boolean isDurable(); - - boolean isAutoDelete(); - - Map getArguments(); - -} ----- - -As you can see, an `Exchange` also has a 'type' represented by constants defined in `ExchangeTypes`. -The basic types are: `direct`, `topic`, `fanout`, and `headers`. -In the core package, you can find implementations of the `Exchange` interface for each of those types. -The behavior varies across these `Exchange` types in terms of how they handle bindings to queues. -For example, a `Direct` exchange lets a queue be bound by a fixed routing key (often the queue's name). -A `Topic` exchange supports bindings with routing patterns that may include the '*' and '#' wildcards for 'exactly-one' and 'zero-or-more', respectively. -The `Fanout` exchange publishes to all queues that are bound to it without taking any routing key into consideration. -For much more information about these and the other Exchange types, see <>. - -NOTE: The AMQP specification also requires that any broker provide a "`default`" direct exchange that has no name. -All queues that are declared are bound to that default `Exchange` with their names as routing keys. -You can learn more about the default Exchange's usage within Spring AMQP in <>. - -===== Queue - -The `Queue` class represents the component from which a message consumer receives messages. -Like the various `Exchange` classes, our implementation is intended to be an abstract representation of this core AMQP type. -The following listing shows the `Queue` class: - -==== -[source,java] ----- -public class Queue { - - private final String name; - - private volatile boolean durable; - - private volatile boolean exclusive; - - private volatile boolean autoDelete; - - private volatile Map arguments; - - /** - * The queue is durable, non-exclusive and non auto-delete. - * - * @param name the name of the queue. - */ - public Queue(String name) { - this(name, true, false, false); - } - - // Getters and Setters omitted for brevity - -} ----- -==== - -Notice that the constructor takes the queue name. -Depending on the implementation, the admin template may provide methods for generating a uniquely named queue. -Such queues can be useful as a "`reply-to`" address or in other *temporary* situations. -For that reason, the 'exclusive' and 'autoDelete' properties of an auto-generated queue would both be set to 'true'. - -NOTE: See the section on queues in <> for information about declaring queues by using namespace support, including queue arguments. - -===== Binding - -Given that a producer sends to an exchange and a consumer receives from a queue, the bindings that connect queues to exchanges are critical for connecting those producers and consumers via messaging. -In Spring AMQP, we define a `Binding` class to represent those connections. -This section reviews the basic options for binding queues to exchanges. - -You can bind a queue to a `DirectExchange` with a fixed routing key, as the following example shows: - -==== -[source,java] ----- -new Binding(someQueue, someDirectExchange, "foo.bar"); ----- -==== - -You can bind a queue to a `TopicExchange` with a routing pattern, as the following example shows: - -==== -[source,java] ----- -new Binding(someQueue, someTopicExchange, "foo.*"); ----- -==== - -You can bind a queue to a `FanoutExchange` with no routing key, as the following example shows: - -==== -[source,java] ----- -new Binding(someQueue, someFanoutExchange); ----- -==== - -We also provide a `BindingBuilder` to facilitate a "`fluent API`" style, as the following example shows: - -==== -[source,java] ----- -Binding b = BindingBuilder.bind(someQueue).to(someTopicExchange).with("foo.*"); ----- -==== - -NOTE: For clarity, the preceding example shows the `BindingBuilder` class, but this style works well when using a static import for the 'bind()' method. - -By itself, an instance of the `Binding` class only holds the data about a connection. -In other words, it is not an "`active`" component. -However, as you will see later in <>, the `AmqpAdmin` class can use `Binding` instances to actually trigger the binding actions on the broker. -Also, as you can see in that same section, you can define the `Binding` instances by using Spring's `@Bean` annotations within `@Configuration` classes. -There is also a convenient base class that further simplifies that approach for generating AMQP-related bean definitions and recognizes the queues, exchanges, and bindings so that they are all declared on the AMQP broker upon application startup. - -The `AmqpTemplate` is also defined within the core package. -As one of the main components involved in actual AMQP messaging, it is discussed in detail in its own section (see <>). - -[[connections]] -==== Connection and Resource Management - -Whereas the AMQP model we described in the previous section is generic and applicable to all implementations, when we get into the management of resources, the details are specific to the broker implementation. -Therefore, in this section, we focus on code that exists only within our "`spring-rabbit`" module since, at this point, RabbitMQ is the only supported implementation. - -The central component for managing a connection to the RabbitMQ broker is the `ConnectionFactory` interface. -The responsibility of a `ConnectionFactory` implementation is to provide an instance of `org.springframework.amqp.rabbit.connection.Connection`, which is a wrapper for `com.rabbitmq.client.Connection`. - -[[choosing-factory]] -===== Choosing a Connection Factory - -There are three connection factories to chose from - -* `PooledChannelConnectionFactory` -* `ThreadChannelConnectionFactory` -* `CachingConnectionFactory` - -The first two were added in version 2.3. - -For most use cases, the `PooledChannelConnectionFactory` should be used. -The `ThreadChannelConnectionFactory` can be used if you want to ensure strict message ordering without the need to use <>. -The `CachingConnectionFactory` should be used if you want to use correlated publisher confirmations or if you wish to open multiple connections, via its `CacheMode`. - -Simple publisher confirmations are supported by all three factories. - -When configuring a `RabbitTemplate` to use a <>, you can now, starting with version 2.3.2, configure the publishing connection factory to be a different type. -By default, the publishing factory is the same type and any properties set on the main factory are also propagated to the publishing factory. - -====== `PooledChannelConnectionFactory` - -This factory manages a single connection and two pools of channels, based on the Apache Pool2. -One pool is for transactional channels, the other is for non-transactional channels. -The pools are `GenericObjectPool` s with default configuration; a callback is provided to configure the pools; refer to the Apache documentation for more information. - -The Apache `commons-pool2` jar must be on the class path to use this factory. - -==== -[source, java] ----- -@Bean -PooledChannelConnectionFactory pcf() throws Exception { - ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); - rabbitConnectionFactory.setHost("localhost"); - PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(rabbitConnectionFactory); - pcf.setPoolConfigurer((pool, tx) -> { - if (tx) { - // configure the transactional pool - } - else { - // configure the non-transactional pool - } - }); - return pcf; -} ----- -==== - -====== `ThreadChannelConnectionFactory` - -This factory manages a single connection and two `ThreadLocal` s, one for transactional channels, the other for non-transactional channels. -This factory ensures that all operations on the same thread use the same channel (as long as it remains open). -This facilitates strict message ordering without the need for <>. -To avoid memory leaks, if your application uses many short-lived threads, you must call the factory's `closeThreadChannel()` to release the channel resource. -Starting with version 2.3.7, a thread can transfer its channel(s) to another thread. -See <> for more information. - -====== `CachingConnectionFactory` - -The third implementation provided is the `CachingConnectionFactory`, which, by default, establishes a single connection proxy that can be shared by the application. -Sharing of the connection is possible since the "`unit of work`" for messaging with AMQP is actually a "`channel`" (in some ways, this is similar to the relationship between a connection and a session in JMS). -The connection instance provides a `createChannel` method. -The `CachingConnectionFactory` implementation supports caching of those channels, and it maintains separate caches for channels based on whether they are transactional. -When creating an instance of `CachingConnectionFactory`, you can provide the 'hostname' through the constructor. -You should also provide the 'username' and 'password' properties. -To configure the size of the channel cache (the default is 25), you can call the -`setChannelCacheSize()` method. - -Starting with version 1.3, you can configure the `CachingConnectionFactory` to cache connections as well as only channels. -In this case, each call to `createConnection()` creates a new connection (or retrieves an idle one from the cache). -Closing a connection returns it to the cache (if the cache size has not been reached). -Channels created on such connections are also cached. -The use of separate connections might be useful in some environments, such as consuming from an HA cluster, in -conjunction with a load balancer, to connect to different cluster members, and others. -To cache connections, set the `cacheMode` to `CacheMode.CONNECTION`. - -NOTE: This does not limit the number of connections. -Rather, it specifies how many idle open connections are allowed. - -Starting with version 1.5.5, a new property called `connectionLimit` is provided. -When this property is set, it limits the total number of connections allowed. -When set, if the limit is reached, the `channelCheckoutTimeLimit` is used to wait for a connection to become idle. -If the time is exceeded, an `AmqpTimeoutException` is thrown. - -[IMPORTANT] -====== -When the cache mode is `CONNECTION`, automatic declaration of queues and others -(See <>) is NOT supported. - -Also, at the time of this writing, the `amqp-client` library by default creates a fixed thread pool for each connection (default size: `Runtime.getRuntime().availableProcessors() * 2` threads). -When using a large number of connections, you should consider setting a custom `executor` on the `CachingConnectionFactory`. -Then, the same executor can be used by all connections and its threads can be shared. -The executor's thread pool should be unbounded or set appropriately for the expected use (usually, at least one thread per connection). -If multiple channels are created on each connection, the pool size affects the concurrency, so a variable (or simple cached) thread pool executor would be most suitable. -====== - -It is important to understand that the cache size is (by default) not a limit but is merely the number of channels that can be cached. -With a cache size of, say, 10, any number of channels can actually be in use. -If more than 10 channels are being used and they are all returned to the cache, 10 go in the cache. -The remainder are physically closed. - -Starting with version 1.6, the default channel cache size has been increased from 1 to 25. -In high volume, multi-threaded environments, a small cache means that channels are created and closed at a high rate. -Increasing the default cache size can avoid this overhead. -You should monitor the channels in use through the RabbitMQ Admin UI and consider increasing the cache size further if you -see many channels being created and closed. -The cache grows only on-demand (to suit the concurrency requirements of the application), so this change does not -impact existing low-volume applications. - -Starting with version 1.4.2, the `CachingConnectionFactory` has a property called `channelCheckoutTimeout`. -When this property is greater than zero, the `channelCacheSize` becomes a limit on the number of channels that can be created on a connection. -If the limit is reached, calling threads block until a channel is available or this timeout is reached, in which case a `AmqpTimeoutException` is thrown. - -WARNING: Channels used within the framework (for example, -`RabbitTemplate`) are reliably returned to the cache. -If you create channels outside of the framework, (for example, -by accessing the connections directly and invoking `createChannel()`), you must return them (by closing) reliably, perhaps in a `finally` block, to avoid running out of channels. - -The following example shows how to create a new `connection`: - -==== -[source,java] ----- -CachingConnectionFactory connectionFactory = new CachingConnectionFactory("somehost"); -connectionFactory.setUsername("guest"); -connectionFactory.setPassword("guest"); - -Connection connection = connectionFactory.createConnection(); ----- -==== - -==== -When using XML, the configuration might look like the following example: - -[source,xml] ----- - - - - - ----- -==== - -NOTE: There is also a `SingleConnectionFactory` implementation that is available only in the unit test code of the framework. -It is simpler than `CachingConnectionFactory`, since it does not cache channels, but it is not intended for practical usage outside of simple tests due to its lack of performance and resilience. -If you need to implement your own `ConnectionFactory` for some reason, the `AbstractConnectionFactory` base class may provide a nice starting point. - -A `ConnectionFactory` can be created quickly and conveniently by using the rabbit namespace, as follows: - -==== -[source,xml] ----- - ----- -==== - -In most cases, this approach is preferable, since the framework can choose the best defaults for you. -The created instance is a `CachingConnectionFactory`. -Keep in mind that the default cache size for channels is 25. -If you want more channels to be cachedm, set a larger value by setting the 'channelCacheSize' property. -In XML it would look like as follows: - -==== -[source,xml] ----- - - - - - - ----- -==== - -Also, with the namespace, you can add the 'channel-cache-size' attribute, as follows: - -==== -[source,xml] ----- - ----- -==== - -The default cache mode is `CHANNEL`, but you can configure it to cache connections instead. -In the following example, we use `connection-cache-size`: - -==== -[source,xml] ----- - ----- -==== - -You can provide host and port attributes by using the namespace, as follows: - -==== -[source,xml] ----- - ----- -==== - -Alternatively, if running in a clustered environment, you can use the addresses attribute, as follows: - -==== -[source,xml] ----- - ----- -==== - -See <> for information about `address-shuffle-mode`. - -The following example with a custom thread factory that prefixes thread names with `rabbitmq-`: - -==== -[source, xml] ----- - - - - - - ----- -==== - -===== AddressResolver - -Starting with version 2.1.15, you can now use an `AddressResover` to resolve the connection address(es). -This will override any settings of the `addresses` and `host/port` properties. - -===== Naming Connections - -Starting with version 1.7, a `ConnectionNameStrategy` is provided for the injection into the `AbstractionConnectionFactory`. -The generated name is used for the application-specific identification of the target RabbitMQ connection. -The connection name is displayed in the management UI if the RabbitMQ server supports it. -This value does not have to be unique and cannot be used as a connection identifier -- for example, in HTTP API requests. -This value is supposed to be human-readable and is a part of `ClientProperties` under the `connection_name` key. -You can use a simple Lambda, as follows: - -==== -[source, java] ----- -connectionFactory.setConnectionNameStrategy(connectionFactory -> "MY_CONNECTION"); ----- -==== - -The `ConnectionFactory` argument can be used to distinguish target connection names by some logic. -By default, the `beanName` of the `AbstractConnectionFactory`, a hex string representing the object, and an internal counter are used to generate the `connection_name`. -The `` namespace component is also supplied with the `connection-name-strategy` attribute. - -An implementation of `SimplePropertyValueConnectionNameStrategy` sets the connection name to an application property. -You can declare it as a `@Bean` and inject it into the connection factory, as the following example shows: - -==== -[source, java] ----- -@Bean -public SimplePropertyValueConnectionNameStrategy cns() { - return new SimplePropertyValueConnectionNameStrategy("spring.application.name"); -} - -@Bean -public ConnectionFactory rabbitConnectionFactory(ConnectionNameStrategy cns) { - CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); - ... - connectionFactory.setConnectionNameStrategy(cns); - return connectionFactory; -} ----- -==== - -The property must exist in the application context's `Environment`. - -NOTE: When using Spring Boot and its autoconfigured connection factory, you need only declare the `ConnectionNameStrategy` `@Bean`. -Boot auto-detects the bean and wires it into the factory. - -===== Blocked Connections and Resource Constraints - -The connection might be blocked for interaction from the broker that corresponds to the https://www.rabbitmq.com/memory.html[Memory Alarm]. -Starting with version 2.0, the `org.springframework.amqp.rabbit.connection.Connection` can be supplied with `com.rabbitmq.client.BlockedListener` instances to be notified for connection blocked and unblocked events. -In addition, the `AbstractConnectionFactory` emits a `ConnectionBlockedEvent` and `ConnectionUnblockedEvent`, respectively, through its internal `BlockedListener` implementation. -These let you provide application logic to react appropriately to problems on the broker and (for example) take some corrective actions. - -IMPORTANT: When the application is configured with a single `CachingConnectionFactory`, as it is by default with Spring Boot auto-configuration, the application stops working when the connection is blocked by the Broker. -And when it is blocked by the Broker, any of its clients stop to work. -If we have producers and consumers in the same application, we may end up with a deadlock when producers are blocking the connection (because there are no resources on the Broker any more) and consumers cannot free them (because the connection is blocked). -To mitigate the problem, we suggest having one more separate `CachingConnectionFactory` instance with the same options -- one for producers and one for consumers. -A separate `CachingConnectionFactory` is not possible for transactional producers that execute on a consumer thread, since they should reuse the `Channel` associated with the consumer transactions. - -Starting with version 2.0.2, the `RabbitTemplate` has a configuration option to automatically use a second connection factory, unless transactions are being used. -See <> for more information. -The `ConnectionNameStrategy` for the publisher connection is the same as the primary strategy with `.publisher` appended to the result of calling the method. - -Starting with version 1.7.7, an `AmqpResourceNotAvailableException` is provided, which is thrown when `SimpleConnection.createChannel()` cannot create a `Channel` (for example, because the `channelMax` limit is reached and there are no available channels in the cache). -You can use this exception in the `RetryPolicy` to recover the operation after some back-off. - -[[connection-factory]] -===== Configuring the Underlying Client Connection Factory - -The `CachingConnectionFactory` uses an instance of the Rabbit client `ConnectionFactory`. -A number of configuration properties are passed through (`host, port, userName, password, requestedHeartBeat, and connectionTimeout` for example) when setting the equivalent property on the `CachingConnectionFactory`. -To set other properties (`clientProperties`, for example), you can define an instance of the Rabbit factory and provide a reference to it by using the appropriate constructor of the `CachingConnectionFactory`. -When using the namespace (<>), you need to provide a reference to the configured factory in the `connection-factory` attribute. -For convenience, a factory bean is provided to assist in configuring the connection factory in a Spring application context, as discussed in <>. - -==== -[source,xml] ----- - ----- -==== - -NOTE: The 4.0.x client enables automatic recovery by default. -While compatible with this feature, Spring AMQP has its own recovery mechanisms and the client recovery feature generally is not needed. -We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. -You may notice this exception, for example, when a `RetryTemplate` is configured in a `RabbitTemplate`, even when failing over to another broker in a cluster. -Since the auto-recovering connection recovers on a timer, the connection may be recovered more quickly by using Spring AMQP's recovery mechanisms. -Starting with version 1.7.1, Spring AMQP disables `amqp-client` automatic recovery unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. -RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. - -[[rabbitconnectionfactorybean-configuring-ssl]] -===== `RabbitConnectionFactoryBean` and Configuring SSL - -Starting with version 1.4, a convenient `RabbitConnectionFactoryBean` is provided to enable convenient configuration of SSL properties on the underlying client connection factory by using dependency injection. -Other setters delegate to the underlying factory. -Previously, you had to configure the SSL options programmatically. -The following example shows how to configure a `RabbitConnectionFactoryBean`: - -==== -[source,xml] ----- - - - - - - ----- -==== - -See the https://www.rabbitmq.com/ssl.html[RabbitMQ Documentation] for information about configuring SSL. -Omit the `keyStore` and `trustStore` configuration to connect over SSL without certificate validation. -The next example shows how you can provide key and trust store configuration. - -The `sslPropertiesLocation` property is a Spring `Resource` pointing to a properties file containing the following keys: - -==== -[source] ----- -keyStore=file:/secret/keycert.p12 -trustStore=file:/secret/trustStore -keyStore.passPhrase=secret -trustStore.passPhrase=secret ----- -==== - -The `keyStore` and `truststore` are Spring `Resources` pointing to the stores. -Typically this properties file is secured by the operating system with the application having read access. - -Starting with Spring AMQP version 1.5,you can set these properties directly on the factory bean. -If both discrete properties and `sslPropertiesLocation` is provided, properties in the latter override the -discrete values. - -IMPORTANT: Starting with version 2.0, the server certificate is validated by default because it is more secure. -If you wish to skip this validation for some reason, set the factory bean's `skipServerCertificateValidation` property to `true`. -Starting with version 2.1, the `RabbitConnectionFactoryBean` now calls `enableHostnameVerification()` by default. -To revert to the previous behavior, set the `enableHostnameVerification` property to `false`. - -IMPORTANT: Starting with version 2.2.5, the factory bean will always use TLS v1.2 by default; previously, it used v1.1 in some cases and v1.2 in others (depending on other properties). -If you need to use v1.1 for some reason, set the `sslAlgorithm` property: `setSslAlgorithm("TLSv1.1")`. - -[[cluster]] -===== Connecting to a Cluster - -To connect to a cluster, configure the `addresses` property on the `CachingConnectionFactory`: - -==== -[source, java] ----- -@Bean -public CachingConnectionFactory ccf() { - CachingConnectionFactory ccf = new CachingConnectionFactory(); - ccf.setAddresses("host1:5672,host2:5672,host3:5672"); - return ccf; -} ----- -==== - -The underlying connection factory will attempt to connect to each host, in order, whenever a new connection is established. -Starting with version 2.1.8, the connection order can be made random by setting the `addressShuffleMode` property to `RANDOM`; the shuffle will be applied before creating any new connection. -Starting with version 2.6, the `INORDER` shuffle mode was added, which means the first address is moved to the end after a connection is created. -You may wish to use this mode with the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `CacheMode.CONNECTION` and suitable concurrency if you wish to consume from all shards on all nodes. - -==== -[source, java] ----- -@Bean -public CachingConnectionFactory ccf() { - CachingConnectionFactory ccf = new CachingConnectionFactory(); - ccf.setAddresses("host1:5672,host2:5672,host3:5672"); - ccf.setAddressShuffleMode(AddressShuffleMode.RANDOM); - return ccf; -} ----- -==== - -[[routing-connection-factory]] -===== Routing Connection Factory - -Starting with version 1.3, the `AbstractRoutingConnectionFactory` has been introduced. -This factory provides a mechanism to configure mappings for several `ConnectionFactories` and determine a target `ConnectionFactory` by some `lookupKey` at runtime. -Typically, the implementation checks a thread-bound context. -For convenience, Spring AMQP provides the `SimpleRoutingConnectionFactory`, which gets the current thread-bound `lookupKey` from the `SimpleResourceHolder`. -The following examples shows how to configure a `SimpleRoutingConnectionFactory` in both XML and Java: - -==== -[source,xml] ----- - - - - - - - - - - ----- - -[source,java] ----- -public class MyService { - - @Autowired - private RabbitTemplate rabbitTemplate; - - public void service(String vHost, String payload) { - SimpleResourceHolder.bind(rabbitTemplate.getConnectionFactory(), vHost); - rabbitTemplate.convertAndSend(payload); - SimpleResourceHolder.unbind(rabbitTemplate.getConnectionFactory()); - } - -} ----- -==== - -It is important to unbind the resource after use. -For more information, see the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.html[JavaDoc] for `AbstractRoutingConnectionFactory`. - -Starting with version 1.4, `RabbitTemplate` supports the SpEL `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` properties, which are evaluated on each AMQP protocol interaction operation (`send`, `sendAndReceive`, `receive`, or `receiveAndReply`), resolving to a `lookupKey` value for the provided `AbstractRoutingConnectionFactory`. -You can use bean references, such as `@vHostResolver.getVHost(#root)` in the expression. -For `send` operations, the message to be sent is the root evaluation object. -For `receive` operations, the `queueName` is the root evaluation object. - -The routing algorithm is as follows: If the selector expression is `null` or is evaluated to `null` or the provided `ConnectionFactory` is not an instance of `AbstractRoutingConnectionFactory`, everything works as before, relying on the provided `ConnectionFactory` implementation. -The same occurs if the evaluation result is not `null`, but there is no target `ConnectionFactory` for that `lookupKey` and the `AbstractRoutingConnectionFactory` is configured with `lenientFallback = true`. -In the case of an `AbstractRoutingConnectionFactory`, it does fallback to its `routing` implementation based on `determineCurrentLookupKey()`. -However, if `lenientFallback = false`, an `IllegalStateException` is thrown. - -The namespace support also provides the `send-connection-factory-selector-expression` and `receive-connection-factory-selector-expression` attributes on the `` component. - -Also, starting with version 1.4, you can configure a routing connection factory in a listener container. -In that case, the list of queue names is used as the lookup key. -For example, if you configure the container with `setQueueNames("thing1", "thing2")`, the lookup key is `[thing1,thing]"` (note that there is no space in the key). - -Starting with version 1.6.9, you can add a qualifier to the lookup key by using `setLookupKeyQualifier` on the listener container. -Doing so enables, for example, listening to queues with the same name but in a different virtual host (where you would have a connection factory for each). - -For example, with lookup key qualifier `thing1` and a container listening to queue `thing2`, the lookup key you could register the target connection factory with could be `thing1[thing2]`. - -IMPORTANT: The target (and default, if provided) connection factories must have the same settings for publisher confirms and returns. -See <>. - -[[queue-affinity]] -===== Queue Affinity and the `LocalizedQueueConnectionFactory` - -When using HA queues in a cluster, for the best performance, you may want to connect to the physical broker -where the lead queue resides. -The `CachingConnectionFactory` can be configured with multiple broker addresses. -This is to fail over and the client attempts to connect in order. -The `LocalizedQueueConnectionFactory` uses the REST API provided by the management plugin to determine which node is the lead for the queue. -It then creates (or retrieves from a cache) a `CachingConnectionFactory` that connects to just that node. -If the connection fails, the new lead node is determined and the consumer connects to it. -The `LocalizedQueueConnectionFactory` is configured with a default connection factory, in case the physical location of the queue cannot be determined, in which case it connects as normal to the cluster. - -The `LocalizedQueueConnectionFactory` is a `RoutingConnectionFactory` and the `SimpleMessageListenerContainer` uses the queue names as the lookup key as discussed in <> above. - -NOTE: For this reason (the use of the queue name for the lookup), the `LocalizedQueueConnectionFactory` can only be used if the container is configured to listen to a single queue. - -NOTE: The RabbitMQ management plugin must be enabled on each node. - -CAUTION: This connection factory is intended for long-lived connections, such as those used by the `SimpleMessageListenerContainer`. -It is not intended for short connection use, such as with a `RabbitTemplate` because of the overhead of invoking the REST API before making the connection. -Also, for publish operations, the queue is unknown, and the message is published to all cluster members anyway, so the logic of looking up the node has little value. - -The following example configuration shows how to configure the factories: - -==== -[source, java] ----- -@Autowired -private ConfigurationProperties props; - -@Bean -public CachingConnectionFactory defaultConnectionFactory() { - CachingConnectionFactory cf = new CachingConnectionFactory(); - cf.setAddresses(this.props.getAddresses()); - cf.setUsername(this.props.getUsername()); - cf.setPassword(this.props.getPassword()); - cf.setVirtualHost(this.props.getVirtualHost()); - return cf; -} - -@Bean -public LocalizedQueueConnectionFactory queueAffinityCF( - @Qualifier("defaultConnectionFactory") ConnectionFactory defaultCF) { - return new LocalizedQueueConnectionFactory(defaultCF, - StringUtils.commaDelimitedListToStringArray(this.props.getAddresses()), - StringUtils.commaDelimitedListToStringArray(this.props.getAdminUris()), - StringUtils.commaDelimitedListToStringArray(this.props.getNodes()), - this.props.getVirtualHost(), this.props.getUsername(), this.props.getPassword(), - false, null); -} ----- -==== - -Notice that the first three parameters are arrays of `addresses`, `adminUris`, and `nodes`. -These are positional in that, when a container attempts to connect to a queue, it uses the admin API to determine which node is the lead for the queue and connects to the address in the same array position as that node. - -[[cf-pub-conf-ret]] -===== Publisher Confirms and Returns - -Confirmed (with correlation) and returned messages are supported by setting the `CachingConnectionFactory` property `publisherConfirmType` to `ConfirmType.CORRELATED` and the `publisherReturns` property to 'true'. - -When these options are set, `Channel` instances created by the factory are wrapped in an `PublisherCallbackChannel`, which is used to facilitate the callbacks. -When such a channel is obtained, the client can register a `PublisherCallbackChannel.Listener` with the `Channel`. -The `PublisherCallbackChannel` implementation contains logic to route a confirm or return to the appropriate listener. -These features are explained further in the following sections. - -See also `simplePublisherConfirms` in <>. - -TIP: For some more background information, see the blog post by the RabbitMQ team titled https://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms/[Introducing Publisher Confirms]. - -[[connection-channel-listeners]] -===== Connection and Channel Listeners - -The connection factory supports registering `ConnectionListener` and `ChannelListener` implementations. -This allows you to receive notifications for connection and channel related events. -(A `ConnectionListener` is used by the `RabbitAdmin` to perform declarations when the connection is established - see <> for more information). -The following listing shows the `ConnectionListener` interface definition: - -==== -[source, java] ----- -@FunctionalInterface -public interface ConnectionListener { - - void onCreate(Connection connection); - - default void onClose(Connection connection) { - } - - default void onShutDown(ShutdownSignalException signal) { - } - -} ----- -==== - -Starting with version 2.0, the `org.springframework.amqp.rabbit.connection.Connection` object can be supplied with `com.rabbitmq.client.BlockedListener` instances to be notified for connection blocked and unblocked events. -The following example shows the ChannelListener interface definition: - -==== -[source, java] ----- -@FunctionalInterface -public interface ChannelListener { - - void onCreate(Channel channel, boolean transactional); - - default void onShutDown(ShutdownSignalException signal) { - } - -} ----- -==== - -See <> for one scenario where you might want to register a `ChannelListener`. - -[[channel-close-logging]] -===== Logging Channel Close Events - -Version 1.5 introduced a mechanism to enable users to control logging levels. - -The `CachingConnectionFactory` uses a default strategy to log channel closures as follows: - -* Normal channel closes (200 OK) are not logged. -* If a channel is closed due to a failed passive queue declaration, it is logged at debug level. -* If a channel is closed because the `basic.consume` is refused due to an exclusive consumer condition, it is logged at -INFO level. -* All others are logged at ERROR level. - -To modify this behavior, you can inject a custom `ConditionalExceptionLogger` into the -`CachingConnectionFactory` in its `closeExceptionLogger` property. - -See also <>. - -[[runtime-cache-properties]] -===== Runtime Cache Properties - -Staring with version 1.6, the `CachingConnectionFactory` now provides cache statistics through the `getCacheProperties()` -method. -These statistics can be used to tune the cache to optimize it in production. -For example, the high water marks can be used to determine whether the cache size should be increased. -If it equals the cache size, you might want to consider increasing further. -The following table describes the `CacheMode.CHANNEL` properties: - -.Cache properties for CacheMode.CHANNEL -[cols="2l,4", options="header"] -|=== -|Property - -|Meaning - -|connectionName - -|The name of the connection generated by the `ConnectionNameStrategy`. - -|channelCacheSize - -|The currently configured maximum channels that are allowed to be idle. - -|localPort - -|The local port for the connection (if available). -This can be used to correlate with connections and channels on the RabbitMQ Admin UI. - -|idleChannelsTx - -|The number of transactional channels that are currently idle (cached). - -|idleChannelsNotTx - -|The number of non-transactional channels that are currently idle (cached). - -|idleChannelsTxHighWater - -|The maximum number of transactional channels that have been concurrently idle (cached). - -|idleChannelsNotTxHighWater - -|The maximum number of non-transactional channels have been concurrently idle (cached). - -|=== - -The following table describes the `CacheMode.CONNECTION` properties: - -.Cache properties for CacheMode.CONNECTION -[cols="2l,4", options="header"] -|=== -|Property - -|Meaning - -|connectionName: - -|The name of the connection generated by the `ConnectionNameStrategy`. - -|openConnections - -|The number of connection objects representing connections to brokers. - -|channelCacheSize - -|The currently configured maximum channels that are allowed to be idle. - -|connectionCacheSize - -|The currently configured maximum connections that are allowed to be idle. - -|idleConnections - -|The number of connections that are currently idle. - -|idleConnectionsHighWater - -|The maximum number of connections that have been concurrently idle. - -|idleChannelsTx: - -|The number of transactional channels that are currently idle (cached) for this connection. -You can use the `localPort` part of the property name to correlate with connections and channels on the RabbitMQ Admin UI. - -|idleChannelsNotTx: - -|The number of non-transactional channels that are currently idle (cached) for this connection. -The `localPort` part of the property name can be used to correlate with connections and channels on the RabbitMQ Admin UI. - -|idleChannelsTxHighWater: - -|The maximum number of transactional channels that have been concurrently idle (cached). -The localPort part of the property name can be used to correlate with connections and channels on the RabbitMQ Admin UI. - -|idleChannelsNotTxHighWater: - -|The maximum number of non-transactional channels have been concurrently idle (cached). -You can use the `localPort` part of the property name to correlate with connections and channels on the RabbitMQ Admin UI. - -|=== - -The `cacheMode` property (`CHANNEL` or `CONNECTION`) is also included. - -.JVisualVM Example -image::images/cacheStats.png[align="center"] - -[[auto-recovery]] -===== RabbitMQ Automatic Connection/Topology recovery - -Since the first version of Spring AMQP, the framework has provided its own connection and channel recovery in the event of a broker failure. -Also, as discussed in <>, the `RabbitAdmin` re-declares any infrastructure beans (queues and others) when the connection is re-established. -It therefore does not rely on the https://www.rabbitmq.com/api-guide.html#recovery[auto-recovery] that is now provided by the `amqp-client` library. -Spring AMQP now uses the `4.0.x` version of `amqp-client`, which has auto recovery enabled by default. -Spring AMQP can still use its own recovery mechanisms if you wish, disabling it in the client, (by setting the `automaticRecoveryEnabled` property on the underlying `RabbitMQ connectionFactory` to `false`). -However, the framework is completely compatible with auto-recovery being enabled. -This means any consumers you create within your code (perhaps via `RabbitTemplate.execute()`) can be recovered automatically. - -IMPORTANT: Only elements (queues, exchanges, bindings) that are defined as beans will be re-declared after a connection failure. -Elements declared by invoking `RabbitAdmin.declare*()` methods directly from user code are unknown to the framework and therefore cannot be recovered. -If you have a need for a variable number of declarations, consider defining a bean, or beans, of type `Declarables`, as discussed in <>. - -[[custom-client-props]] -==== Adding Custom Client Connection Properties - -The `CachingConnectionFactory` now lets you access the underlying connection factory to allow, for example, -setting custom client properties. -The following example shows how to do so: - -[source, java] ----- -connectionFactory.getRabbitConnectionFactory().getClientProperties().put("thing1", "thing2"); ----- - -These properties appear in the RabbitMQ Admin UI when viewing the connection. - -[[amqp-template]] -==== `AmqpTemplate` - -As with many other high-level abstractions provided by the Spring Framework and related projects, Spring AMQP provides a "`template`" that plays a central role. -The interface that defines the main operations is called `AmqpTemplate`. -Those operations cover the general behavior for sending and receiving messages. -In other words, they are not unique to any implementation -- hence the "`AMQP`" in the name. -On the other hand, there are implementations of that interface that are tied to implementations of the AMQP protocol. -Unlike JMS, which is an interface-level API itself, AMQP is a wire-level protocol. -The implementations of that protocol provide their own client libraries, so each implementation of the template interface depends on a particular client library. -Currently, there is only a single implementation: `RabbitTemplate`. -In the examples that follow, we often use an `AmqpTemplate`. -However, when you look at the configuration examples or any code excerpts where the template is instantiated or setters are invoked, you can see the implementation type (for example, `RabbitTemplate`). - -As mentioned earlier, the `AmqpTemplate` interface defines all of the basic operations for sending and receiving messages. -We will explore message sending and reception, respectively, in <> and <>. - -See also <>. - -[[template-retry]] -===== Adding Retry Capabilities - -Starting with version 1.3, you can now configure the `RabbitTemplate` to use a `RetryTemplate` to help with handling problems with broker connectivity. -See the https://github.com/spring-projects/spring-retry[spring-retry] project for complete information. -The following is only one example that uses an exponential back off policy and the default `SimpleRetryPolicy`, which makes three tries before throwing the exception to the caller. - -The following example uses the XML namespace: - -==== -[source,xml] ----- - - - - - - - - - - - ----- -==== - -The following example uses the `@Configuration` annotation in Java: - -==== -[source,java] ----- -@Bean -public RabbitTemplate rabbitTemplate() { - RabbitTemplate template = new RabbitTemplate(connectionFactory()); - RetryTemplate retryTemplate = new RetryTemplate(); - ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); - backOffPolicy.setInitialInterval(500); - backOffPolicy.setMultiplier(10.0); - backOffPolicy.setMaxInterval(10000); - retryTemplate.setBackOffPolicy(backOffPolicy); - template.setRetryTemplate(retryTemplate); - return template; -} ----- -==== - -Starting with version 1.4, in addition to the `retryTemplate` property, the `recoveryCallback` option is supported on the `RabbitTemplate`. -It is used as a second argument for the `RetryTemplate.execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback)`. - -NOTE: The `RecoveryCallback` is somewhat limited, in that the retry context contains only the `lastThrowable` field. -For more sophisticated use cases, you should use an external `RetryTemplate` so that you can convey additional information to the `RecoveryCallback` through the context's attributes. -The following example shows how to do so: - -==== -[source,java] ----- -retryTemplate.execute( - new RetryCallback() { - - @Override - public Object doWithRetry(RetryContext context) throws Exception { - context.setAttribute("message", message); - return rabbitTemplate.convertAndSend(exchange, routingKey, message); - } - - }, new RecoveryCallback() { - - @Override - public Object recover(RetryContext context) throws Exception { - Object message = context.getAttribute("message"); - Throwable t = context.getLastThrowable(); - // Do something with message - return null; - } - }); -} ----- -==== - -In this case, you would *not* inject a `RetryTemplate` into the `RabbitTemplate`. - -[[publishing-is-async]] -===== Publishing is Asynchronous -- How to Detect Successes and Failures - -Publishing messages is an asynchronous mechanism and, by default, messages that cannot be routed are dropped by RabbitMQ. -For successful publishing, you can receive an asynchronous confirm, as described in <>. -Consider two failure scenarios: - -* Publish to an exchange but there is no matching destination queue. -* Publish to a non-existent exchange. - -The first case is covered by publisher returns, as described in <>. - -For the second case, the message is dropped and no return is generated. -The underlying channel is closed with an exception. -By default, this exception is logged, but you can register a `ChannelListener` with the `CachingConnectionFactory` to obtain notifications of such events. -The following example shows how to add a `ConnectionListener`: - -==== -[source, java] ----- -this.connectionFactory.addConnectionListener(new ConnectionListener() { - - @Override - public void onCreate(Connection connection) { - } - - @Override - public void onShutDown(ShutdownSignalException signal) { - ... - } - -}); ----- -==== - -You can examine the signal's `reason` property to determine the problem that occurred. - -To detect the exception on the sending thread, you can `setChannelTransacted(true)` on the `RabbitTemplate` and the exception is detected on the `txCommit()`. -However, *transactions significantly impede performance*, so consider this carefully before enabling transactions for just this one use case. - -[[template-confirms]] -===== Correlated Publisher Confirms and Returns - -The `RabbitTemplate` implementation of `AmqpTemplate` supports publisher confirms and returns. - -For returned messages, the template's `mandatory` property must be set to `true` or the `mandatory-expression` -must evaluate to `true` for a particular message. -This feature requires a `CachingConnectionFactory` that has its `publisherReturns` property set to `true` (see <>). -Returns are sent to the client by it registering a `RabbitTemplate.ReturnsCallback` by calling `setReturnsCallback(ReturnsCallback callback)`. -The callback must implement the following method: - -==== -[source,java] ----- -void returnedMessage(ReturnedMessage returned); ----- -==== - -The `ReturnedMessage` has the following properties: - -- `message` - the returned message itself -- `replyCode` - a code indicating the reason for the return -- `replyText` - a textual reason for the return - e.g. `NO_ROUTE` -- `exchange` - the exchange to which the message was sent -- `routingKey` - the routing key that was used - -Only one `ReturnsCallback` is supported by each `RabbitTemplate`. -See also <>. - -For publisher confirms (also known as publisher acknowledgements), the template requires a `CachingConnectionFactory` that has its `publisherConfirm` property set to `ConfirmType.CORRELATED`. -Confirms are sent to the client by it registering a `RabbitTemplate.ConfirmCallback` by calling `setConfirmCallback(ConfirmCallback callback)`. -The callback must implement this method: - -==== -[source,java] ----- -void confirm(CorrelationData correlationData, boolean ack, String cause); ----- -==== - -The `CorrelationData` is an object supplied by the client when sending the original message. -The `ack` is true for an `ack` and false for a `nack`. -For `nack` instances, the cause may contain a reason for the `nack`, if it is available when the `nack` is generated. -An example is when sending a message to a non-existent exchange. -In that case, the broker closes the channel. -The reason for the closure is included in the `cause`. -The `cause` was added in version 1.4. - -Only one `ConfirmCallback` is supported by a `RabbitTemplate`. - -NOTE: When a rabbit template send operation completes, the channel is closed. -This precludes the reception of confirms or returns when the connection factory cache is full (when there is space in the cache, the channel is not physically closed and the returns and confirms proceed normally). -When the cache is full, the framework defers the close for up to five seconds, in order to allow time for the confirms and returns to be received. -When using confirms, the channel is closed when the last confirm is received. -When using only returns, the channel remains open for the full five seconds. -We generally recommend setting the connection factory's `channelCacheSize` to a large enough value so that the channel on which a message is published is returned to the cache instead of being closed. -You can monitor channel usage by using the RabbitMQ management plugin. -If you see channels being opened and closed rapidly, you should consider increasing the cache size to reduce overhead on the server. - -IMPORTANT: Before version 2.1, channels enabled for publisher confirms were returned to the cache before the confirms were received. -Some other process could check out the channel and perform some operation that causes the channel to close -- such as publishing a message to a non-existent exchange. -This could cause the confirm to be lost. -Version 2.1 and later no longer return the channel to the cache while confirms are outstanding. -The `RabbitTemplate` performs a logical `close()` on the channel after each operation. -In general, this means that only one confirm is outstanding on a channel at a time. - -NOTE: Starting with version 2.2, the callbacks are invoked on one of the connection factory's `executor` threads. -This is to avoid a potential deadlock if you perform Rabbit operations from within the callback. -With previous versions, the callbacks were invoked directly on the `amqp-client` connection I/O thread; this would deadlock if you perform some RPC operation (such as opening a new channel) since the I/O thread blocks waiting for the result, but the result needs to be processed by the I/O thread itself. -With those versions, it was necessary to hand off work (such as sending a messasge) to another thread within the callback. -This is no longer necessary since the framework now hands off the callback invocation to the executor. - -IMPORTANT: The guarantee of receiving a returned message before the ack is still maintained as long as the return callback executes in 60 seconds or less. -The confirm is scheduled to be delivered after the return callback exits or after 60 seconds, whichever comes first. - -Starting with version 2.1, the `CorrelationData` object has a `ListenableFuture` that you can use to get the result, instead of using a `ConfirmCallback` on the template. -The following example shows how to configure a `CorrelationData` instance: - -==== -[source, java] ----- -CorrelationData cd1 = new CorrelationData(); -this.templateWithConfirmsEnabled.convertAndSend("exchange", queue.getName(), "foo", cd1); -assertTrue(cd1.getFuture().get(10, TimeUnit.SECONDS).isAck()); ----- -==== - -Since it is a `ListenableFuture`, you can either `get()` the result when ready or add listeners for an asynchronous callback. -The `Confirm` object is a simple bean with 2 properties: `ack` and `reason` (for `nack` instances). -The reason is not populated for broker-generated `nack` instances. -It is populated for `nack` instances generated by the framework (for example, closing the connection while `ack` instances are outstanding). - -In addition, when both confirms and returns are enabled, the `CorrelationData` is populated with the returned message, as long as the `CorrelationData` has a unique `id`; this is always the case, by default, starting with version 2.3. -It is guaranteed that the returned message is set before the future is set with the `ack`. - -See also <> for a simpler mechanism for waiting for publisher confirms. - -[[scoped-operations]] -===== Scoped Operations - -Normally, when using the template, a `Channel` is checked out of the cache (or created), used for the operation, and returned to the cache for reuse. -In a multi-threaded environment, there is no guarantee that the next operation uses the same channel. -There may be times, however, where you want to have more control over the use of a channel and ensure that a number of operations are all performed on the same channel. - -Starting with version 2.0, a new method called `invoke` is provided, with an `OperationsCallback`. -Any operations performed within the scope of the callback and on the provided `RabbitOperations` argument use the same dedicated `Channel`, which will be closed at the end (not returned to a cache). -If the channel is a `PublisherCallbackChannel`, it is returned to the cache after all confirms have been received (see <>). - -==== -[source, java] ----- -@FunctionalInterface -public interface OperationsCallback { - - T doInRabbit(RabbitOperations operations); - -} ----- -==== - -One example of why you might need this is if you wish to use the `waitForConfirms()` method on the underlying `Channel`. -This method was not previously exposed by the Spring API because the channel is, generally, cached and shared, as discussed earlier. -The `RabbitTemplate` now provides `waitForConfirms(long timeout)` and `waitForConfirmsOrDie(long timeout)`, which delegate to the dedicated channel used within the scope of the `OperationsCallback`. -The methods cannot be used outside of that scope, for obvious reasons. - -Note that a higher-level abstraction that lets you correlate confirms to requests is provided elsewhere (see <>). -If you want only to wait until the broker has confirmed delivery, you can use the technique shown in the following example: - -==== -[source, java] ----- -Collection messages = getMessagesToSend(); -Boolean result = this.template.invoke(t -> { - messages.forEach(m -> t.convertAndSend(ROUTE, m)); - t.waitForConfirmsOrDie(10_000); - return true; -}); ----- -==== - -If you wish `RabbitAdmin` operations to be invoked on the same channel within the scope of the `OperationsCallback`, the admin must have been constructed by using the same `RabbitTemplate` that was used for the `invoke` operation. - -NOTE: The preceding discussion is moot if the template operations are already performed within the scope of an existing transaction -- for example, when running on a transacted listener container thread and performing operations on a transacted template. -In that case, the operations are performed on that channel and committed when the thread returns to the container. -It is not necessary to use `invoke` in that scenario. - -When using confirms in this way, much of the infrastructure set up for correlating confirms to requests is not really needed (unless returns are also enabled). -Starting with version 2.2, the connection factory supports a new property called `publisherConfirmType`. -When this is set to `ConfirmType.SIMPLE`, the infrastructure is avoided and the confirm processing can be more efficient. - -Furthermore, the `RabbitTemplate` sets the `publisherSequenceNumber` property in the sent message `MessageProperties`. -If you wish to check (or log or otherwise use) specific confirms, you can do so with an overloaded `invoke` method, as the following example shows: - -==== -[source, java] ----- -public T invoke(OperationsCallback action, com.rabbitmq.client.ConfirmCallback acks, - com.rabbitmq.client.ConfirmCallback nacks); ----- -==== - -NOTE: These `ConfirmCallback` objects (for `ack` and `nack` instances) are the Rabbit client callbacks, not the template callback. - -The following example logs `ack` and `nack` instances: - -==== -[source, java] ----- -Collection messages = getMessagesToSend(); -Boolean result = this.template.invoke(t -> { - messages.forEach(m -> t.convertAndSend(ROUTE, m)); - t.waitForConfirmsOrDie(10_000); - return true; -}, (tag, multiple) -> { - log.info("Ack: " + tag + ":" + multiple); -}, (tag, multiple) -> { - log.info("Nack: " + tag + ":" + multiple); -})); ----- -==== - -IMPORTANT: Scoped operations are bound to a thread. -See <> for a discussion about strict ordering in a multi-threaded environment. - -[[multi-strict]] -===== Strict Message Ordering in a Multi-Threaded Environment - -The discussion in <> applies only when the operations are performed on the same thread. - -Consider the following situation: - -* `thread-1` sends a message to a queue and hands off work to `thread-2` -* `thread-2` sends a message to the same queue - -Because of the async nature of RabbitMQ and the use of cached channels; it is not certain that the same channel will be used and therefore the order in which the messages arrive in the queue is not guaranteed. -(In most cases they will arrive in order, but the probability of out-of-order delivery is not zero). -To solve this use case, you can use a bounded channel cache with size `1` (together with a `channelCheckoutTimeout`) to ensure the messages are always published on the same channel, and order will be guaranteed. -To do this, if you have other uses for the connection factory, such as consumers, you should either use a dedicated connection factory for the template, or configure the template to use the publisher connection factory embedded in the main connection factory (see <>). - -This is best illustrated with a simple Spring Boot Application: - -==== -[source, java] ----- -@SpringBootApplication -public class Application { - - private static final Logger log = LoggerFactory.getLogger(Application.class); - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - TaskExecutor exec() { - ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); - exec.setCorePoolSize(10); - return exec; - } - - @Bean - CachingConnectionFactory ccf() { - CachingConnectionFactory ccf = new CachingConnectionFactory("localhost"); - CachingConnectionFactory publisherCF = (CachingConnectionFactory) ccf.getPublisherConnectionFactory(); - publisherCF.setChannelCacheSize(1); - publisherCF.setChannelCheckoutTimeout(1000L); - return ccf; - } - - @RabbitListener(queues = "queue") - void listen(String in) { - log.info(in); - } - - @Bean - Queue queue() { - return new Queue("queue"); - } - - - @Bean - public ApplicationRunner runner(Service service, TaskExecutor exec) { - return args -> { - exec.execute(() -> service.mainService("test")); - }; - } - -} - -@Component -class Service { - - private static final Logger LOG = LoggerFactory.getLogger(Service.class); - - private final RabbitTemplate template; - - private final TaskExecutor exec; - - Service(RabbitTemplate template, TaskExecutor exec) { - template.setUsePublisherConnection(true); - this.template = template; - this.exec = exec; - } - - void mainService(String toSend) { - LOG.info("Publishing from main service"); - this.template.convertAndSend("queue", toSend); - this.exec.execute(() -> secondaryService(toSend.toUpperCase())); - } - - void secondaryService(String toSend) { - LOG.info("Publishing from secondary service"); - this.template.convertAndSend("queue", toSend); - } - -} ----- -==== - -Even though the publishing is performed on two different threads, they will both use the same channel because the cache is capped at a single channel. - -Starting with version 2.3.7, the `ThreadChannelConnectionFactory` supports transferring a thread's channel(s) to another thread, using the `prepareContextSwitch` and `switchContext` methods. -The first method returns a context which is passed to the second thread which calls the second method. -A thread can have either a non-transactional channel or a transactional channel (or one of each) bound to it; you cannot transfer them individually, unless you use two connection factories. -An example follows: - -==== -[source, java] ----- -@SpringBootApplication -public class Application { - - private static final Logger log = LoggerFactory.getLogger(Application.class); - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - TaskExecutor exec() { - ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); - exec.setCorePoolSize(10); - return exec; - } - - @Bean - ThreadChannelConnectionFactory tccf() { - ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); - rabbitConnectionFactory.setHost("localhost"); - return new ThreadChannelConnectionFactory(rabbitConnectionFactory); - } - - @RabbitListener(queues = "queue") - void listen(String in) { - log.info(in); - } - - @Bean - Queue queue() { - return new Queue("queue"); - } - - - @Bean - public ApplicationRunner runner(Service service, TaskExecutor exec) { - return args -> { - exec.execute(() -> service.mainService("test")); - }; - } - -} - -@Component -class Service { - - private static final Logger LOG = LoggerFactory.getLogger(Service.class); - - private final RabbitTemplate template; - - private final TaskExecutor exec; - - private final ThreadChannelConnectionFactory connFactory; - - Service(RabbitTemplate template, TaskExecutor exec, - ThreadChannelConnectionFactory tccf) { - - this.template = template; - this.exec = exec; - this.connFactory = tccf; - } - - void mainService(String toSend) { - LOG.info("Publishing from main service"); - this.template.convertAndSend("queue", toSend); - Object context = this.connFactory.prepareSwitchContext(); - this.exec.execute(() -> secondaryService(toSend.toUpperCase(), context)); - } - - void secondaryService(String toSend, Object threadContext) { - LOG.info("Publishing from secondary service"); - this.connFactory.switchContext(threadContext); - this.template.convertAndSend("queue", toSend); - this.connFactory.closeThreadChannel(); - } - -} ----- -==== - -IMPORTANT: Once the `prepareSwitchContext` is called, if the current thread performs any more operations, they will be performed on a new channel. -It is important to close the thread-bound channel when it is no longer needed. - -[[template-messaging]] -===== Messaging Integration - -Starting with version 1.4, `RabbitMessagingTemplate` (built on top of `RabbitTemplate`) provides an integration with the Spring Framework messaging abstraction -- that is, -`org.springframework.messaging.Message`. -This lets you send and receive messages by using the `spring-messaging` `Message` abstraction. -This abstraction is used by other Spring projects, such as Spring Integration and Spring's STOMP support. -There are two message converters involved: one to convert between a spring-messaging `Message` and Spring AMQP's `Message` abstraction and one to convert between Spring AMQP's `Message` abstraction and the format required by the underlying RabbitMQ client library. -By default, the message payload is converted by the provided `RabbitTemplate` instance's message converter. -Alternatively, you can inject a custom `MessagingMessageConverter` with some other payload converter, as the following example shows: - -==== -[source, java] ----- -MessagingMessageConverter amqpMessageConverter = new MessagingMessageConverter(); -amqpMessageConverter.setPayloadConverter(myPayloadConverter); -rabbitMessagingTemplate.setAmqpMessageConverter(amqpMessageConverter); ----- -==== - -[[template-user-id]] -===== Validated User Id - -Starting with version 1.6, the template now supports a `user-id-expression` (`userIdExpression` when using Java configuration). -If a message is sent, the user id property is set (if not already set) after evaluating this expression. -The root object for the evaluation is the message to be sent. - -The following examples show how to use the `user-id-expression` attribute: - -==== -[source, xml] ----- - - - ----- -==== - -The first example is a literal expression. -The second obtains the `username` property from a connection factory bean in the application context. - -[[separate-connection]] -===== Using a Separate Connection - -Starting with version 2.0.2, you can set the `usePublisherConnection` property to `true` to use a different connection to that used by listener containers, when possible. -This is to avoid consumers being blocked when a producer is blocked for any reason. -The connection factories maintain a second internal connection factory for this purpose; by default it is the same type as the main factory, but can be set explicity if you wish to use a different factory type for publishing. -If the rabbit template is running in a transaction started by the listener container, the container's channel is used, regardless of this setting. - -IMPORTANT: In general, you should not use a `RabbitAdmin` with a template that has this set to `true`. -Use the `RabbitAdmin` constructor that takes a connection factory. -If you use the other constructor that takes a template, ensure the template's property is `false`. -This is because, often, an admin is used to declare queues for listener containers. -Using a template that has the property set to `true` would mean that exclusive queues (such as `AnonymousQueue`) would be declared on a different connection to that used by listener containers. -In that case, the queues cannot be used by the containers. - -[[sending-messages]] -==== Sending Messages - -When sending a message, you can use any of the following methods: - -==== -[source,java] ----- -void send(Message message) throws AmqpException; - -void send(String routingKey, Message message) throws AmqpException; - -void send(String exchange, String routingKey, Message message) throws AmqpException; ----- -==== - -We can begin our discussion with the last method in the preceding listing, since it is actually the most explicit. -It lets an AMQP exchange name (along with a routing key)be provided at runtime. -The last parameter is the callback that is responsible for actual creating the message instance. -An example of using this method to send a message might look like this: -The following example shows how to use the `send` method to send a message: - -==== -[source,java] ----- -amqpTemplate.send("marketData.topic", "quotes.nasdaq.THING1", - new Message("12.34".getBytes(), someProperties)); ----- -==== - -You can set the `exchange` property on the template itself if you plan to use that template instance to send to the same exchange most or all of the time. -In such cases, you can use the second method in the preceding listing. -The following example is functionally equivalent to the previous example: - -==== -[source,java] ----- -amqpTemplate.setExchange("marketData.topic"); -amqpTemplate.send("quotes.nasdaq.FOO", new Message("12.34".getBytes(), someProperties)); ----- -==== - -If both the `exchange` and `routingKey` properties are set on the template, you can use the method that accepts only the `Message`. -The following example shows how to do so: - -==== -[source,java] ----- -amqpTemplate.setExchange("marketData.topic"); -amqpTemplate.setRoutingKey("quotes.nasdaq.FOO"); -amqpTemplate.send(new Message("12.34".getBytes(), someProperties)); ----- -==== - -A better way of thinking about the exchange and routing key properties is that the explicit method parameters always override the template's default values. -In fact, even if you do not explicitly set those properties on the template, there are always default values in place. -In both cases, the default is an empty `String`, but that is actually a sensible default. -As far as the routing key is concerned, it is not always necessary in the first place (for example, for -a `Fanout` exchange). -Furthermore, a queue may be bound to an exchange with an empty `String`. -Those are both legitimate scenarios for reliance on the default empty `String` value for the routing key property of the template. -As far as the exchange name is concerned, the empty `String` is commonly used because the AMQP specification defines the "`default exchange`" as having no name. -Since all queues are automatically bound to that default exchange (which is a direct exchange), using their name as the binding value, the second method in the preceding listing can be used for simple point-to-point messaging to any queue through the default exchange. -You can provide the queue name as the `routingKey`, either by providing the method parameter at runtime. -The following example shows how to do so: - -==== -[source,java] ----- -RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange -template.send("queue.helloWorld", new Message("Hello World".getBytes(), someProperties)); ----- -==== - -Alternately, you can create a template that can be used for publishing primarily or exclusively to a single Queue. -The following example shows how to do so: - -==== -[source,java] ----- -RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange -template.setRoutingKey("queue.helloWorld"); // but we'll always send to this Queue -template.send(new Message("Hello World".getBytes(), someProperties)); ----- -==== - -[[message-builder]] -===== Message Builder API - -Starting with version 1.3, a message builder API is provided by the `MessageBuilder` and `MessagePropertiesBuilder`. -These methods provide a convenient "`fluent`" means of creating a message or message properties. -The following examples show the fluent API in action: - -==== -[source,java] ----- -Message message = MessageBuilder.withBody("foo".getBytes()) - .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) - .setMessageId("123") - .setHeader("bar", "baz") - .build(); ----- - -[source,java] ----- -MessageProperties props = MessagePropertiesBuilder.newInstance() - .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) - .setMessageId("123") - .setHeader("bar", "baz") - .build(); -Message message = MessageBuilder.withBody("foo".getBytes()) - .andProperties(props) - .build(); ----- -==== - -Each of the properties defined on the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/MessageProperties.html[`MessageProperties`] can be set. -Other methods include `setHeader(String key, String value)`, `removeHeader(String key)`, `removeHeaders()`, and `copyProperties(MessageProperties properties)`. -Each property setting method has a `set*IfAbsent()` variant. -In the cases where a default initial value exists, the method is named `set*IfAbsentOrDefault()`. - -Five static methods are provided to create an initial message builder: - -==== -[source,java] ----- -public static MessageBuilder withBody(byte[] body) <1> - -public static MessageBuilder withClonedBody(byte[] body) <2> - -public static MessageBuilder withBody(byte[] body, int from, int to) <3> - -public static MessageBuilder fromMessage(Message message) <4> - -public static MessageBuilder fromClonedMessage(Message message) <5> ----- - -<1> The message created by the builder has a body that is a direct reference to the argument. -<2> The message created by the builder has a body that is a new array containing a copy of bytes in the argument. -<3> The message created by the builder has a body that is a new array containing the range of bytes from the argument. -See https://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html[`Arrays.copyOfRange()`] for more details. -<4> The message created by the builder has a body that is a direct reference to the body of the argument. -The argument's properties are copied to a new `MessageProperties` object. -<5> The message created by the builder has a body that is a new array containing a copy of the argument's body. -The argument's properties are copied to a new `MessageProperties` object. -==== - -Three static methods are provided to create a `MessagePropertiesBuilder` instance: - -==== -[source,java] ----- -public static MessagePropertiesBuilder newInstance() <1> - -public static MessagePropertiesBuilder fromProperties(MessageProperties properties) <2> - -public static MessagePropertiesBuilder fromClonedProperties(MessageProperties properties) <3> ----- - -<1> A new message properties object is initialized with default values. -<2> The builder is initialized with, and `build()` will return, the provided properties object., -<3> The argument's properties are copied to a new `MessageProperties` object. -==== - -With the `RabbitTemplate` implementation of `AmqpTemplate`, each of the `send()` methods has an overloaded version that takes an additional `CorrelationData` object. -When publisher confirms are enabled, this object is returned in the callback described in <>. -This lets the sender correlate a confirm (`ack` or `nack`) with the sent message. - -Starting with version 1.6.7, the `CorrelationAwareMessagePostProcessor` interface was introduced, allowing the correlation data to be modified after the message has been converted. -The following example shows how to use it: - -==== -[source, java] ----- -Message postProcessMessage(Message message, Correlation correlation); ----- -==== - -In version 2.0, this interface is deprecated. -The method has been moved to `MessagePostProcessor` with a default implementation that delegates to `postProcessMessage(Message message)`. - -Also starting with version 1.6.7, a new callback interface called `CorrelationDataPostProcessor` is provided. -This is invoked after all `MessagePostProcessor` instances (provided in the `send()` method as well as those provided in `setBeforePublishPostProcessors()`). -Implementations can update or replace the correlation data supplied in the `send()` method (if any). -The `Message` and original `CorrelationData` (if any) are provided as arguments. -The following example shows how to use the `postProcess` method: - -==== -[source, java] ----- -CorrelationData postProcess(Message message, CorrelationData correlationData); ----- -==== - -===== Publisher Returns - -When the template's `mandatory` property is `true`, returned messages are provided by the callback described in <>. - -Starting with version 1.4, the `RabbitTemplate` supports the SpEL `mandatoryExpression` property, which is evaluated against each request message as the root evaluation object, resolving to a `boolean` value. -Bean references, such as `@myBean.isMandatory(#root)`, can be used in the expression. - -Publisher returns can also be used internally by the `RabbitTemplate` in send and receive operations. -See <> for more information. - -[[template-batching]] -===== Batching - -Version 1.4.2 introduced the `BatchingRabbitTemplate`. -This is a subclass of `RabbitTemplate` with an overridden `send` method that batches messages according to the `BatchingStrategy`. -Only when a batch is complete is the message sent to RabbitMQ. -The following listing shows the `BatchingStrategy` interface definition: - -==== -[source, java] ----- -public interface BatchingStrategy { - - MessageBatch addToBatch(String exchange, String routingKey, Message message); - - Date nextRelease(); - - Collection releaseBatches(); - -} ----- -==== - -CAUTION: Batched data is held in memory. -Unsent messages can be lost in the event of a system failure. - -A `SimpleBatchingStrategy` is provided. -It supports sending messages to a single exchange or routing key. -It has the following properties: - -* `batchSize`: The number of messages in a batch before it is sent. -* `bufferLimit`: The maximum size of the batched message. -This preempts the `batchSize`, if exceeded, and causes a partial batch to be sent. -* `timeout`: A time after which a partial batch is sent when there is no new activity adding messages to the batch. - -The `SimpleBatchingStrategy` formats the batch by preceding each embedded message with a four-byte binary length. -This is communicated to the receiving system by setting the `springBatchFormat` message property to `lengthHeader4`. - -IMPORTANT: Batched messages are automatically de-batched by listener containers by default (by using the `springBatchFormat` message header). -Rejecting any message from a batch causes the entire batch to be rejected. - -However, see <> for more information. - -[[receiving-messages]] -==== Receiving Messages - -Message reception is always a little more complicated than sending. -There are two ways to receive a `Message`. -The simpler option is to poll for one `Message` at a time with a polling method call. -The more complicated yet more common approach is to register a listener that receives `Messages` on-demand, asynchronously. -We consider an example of each approach in the next two sub-sections. - -[[polling-consumer]] -===== Polling Consumer - -The `AmqpTemplate` itself can be used for polled `Message` reception. -By default, if no message is available, `null` is returned immediately. -There is no blocking. -Starting with version 1.5, you can set a `receiveTimeout`, in milliseconds, and the receive methods block for up to that long, waiting for a message. -A value less than zero means block indefinitely (or at least until the connection to the broker is lost). -Version 1.6 introduced variants of the `receive` methods that let the timeout be passed in on each call. - -CAUTION: Since the receive operation creates a new `QueueingConsumer` for each message, this technique is not really appropriate for high-volume environments. -Consider using an asynchronous consumer or a `receiveTimeout` of zero for those use cases. - -There are four simple `receive` methods available. -As with the `Exchange` on the sending side, there is a method that requires that a default queue property has been set -directly on the template itself, and there is a method that accepts a queue parameter at runtime. -Version 1.6 introduced variants to accept `timeoutMillis` to override `receiveTimeout` on a per-request basis. -The following listing shows the definitions of the four methods: - -==== -[source,java] ----- -Message receive() throws AmqpException; - -Message receive(String queueName) throws AmqpException; - -Message receive(long timeoutMillis) throws AmqpException; - -Message receive(String queueName, long timeoutMillis) throws AmqpException; ----- -==== - -As in the case of sending messages, the `AmqpTemplate` has some convenience methods for receiving POJOs instead of `Message` instances, and implementations provide a way to customize the `MessageConverter` used to create the `Object` returned: -The following listing shows those methods: - -==== -[source,java] ----- -Object receiveAndConvert() throws AmqpException; - -Object receiveAndConvert(String queueName) throws AmqpException; - -Object receiveAndConvert(long timeoutMillis) throws AmqpException; - -Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException; ----- -==== - -Starting with version 2.0, there are variants of these methods that take an additional `ParameterizedTypeReference` argument to convert complex types. -The template must be configured with a `SmartMessageConverter`. -See <> for more information. - -Similar to `sendAndReceive` methods, beginning with version 1.3, the `AmqpTemplate` has several convenience `receiveAndReply` methods for synchronously receiving, processing and replying to messages. -The following listing shows those method definitions: - -==== -[source,java] ----- - boolean receiveAndReply(ReceiveAndReplyCallback callback) - throws AmqpException; - - boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) - throws AmqpException; - - boolean receiveAndReply(ReceiveAndReplyCallback callback, - String replyExchange, String replyRoutingKey) throws AmqpException; - - boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, - String replyExchange, String replyRoutingKey) throws AmqpException; - - boolean receiveAndReply(ReceiveAndReplyCallback callback, - ReplyToAddressCallback replyToAddressCallback) throws AmqpException; - - boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, - ReplyToAddressCallback replyToAddressCallback) throws AmqpException; ----- -==== - -The `AmqpTemplate` implementation takes care of the `receive` and `reply` phases. -In most cases, you should provide only an implementation of `ReceiveAndReplyCallback` to perform some business logic for the received message and build a reply object or message, if needed. -Note, a `ReceiveAndReplyCallback` may return `null`. -In this case, no reply is sent and `receiveAndReply` works like the `receive` method. -This lets the same queue be used for a mixture of messages, some of which may not need a reply. - -Automatic message (request and reply) conversion is applied only if the provided callback is not an instance of `ReceiveAndReplyMessageCallback`, which provides a raw message exchange contract. - -The `ReplyToAddressCallback` is useful for cases requiring custom logic to determine the `replyTo` address at runtime against the received message and reply from the `ReceiveAndReplyCallback`. -By default, `replyTo` information in the request message is used to route the reply. - -The following listing shows an example of POJO-based receive and reply: - -==== -[source,java] ----- -boolean received = - this.template.receiveAndReply(ROUTE, new ReceiveAndReplyCallback() { - - public Invoice handle(Order order) { - return processOrder(order); - } - }); -if (received) { - log.info("We received an order!"); -} ----- -==== - -[[async-consumer]] -===== Asynchronous Consumer - -IMPORTANT: Spring AMQP also supports annotated listener endpoints through the use of the `@RabbitListener` annotation and provides an open infrastructure to register endpoints programmatically. -This is by far the most convenient way to setup an asynchronous consumer. -See <> for more details. - -[IMPORTANT] -==== -The prefetch default value used to be 1, which could lead to under-utilization of efficient consumers. -Starting with version 2.0, the default prefetch value is now 250, which should keep consumers busy in most common scenarios and -thus improve throughput. - -There are, nevertheless, scenarios where the prefetch value should be low: - -* For large messages, especially if the processing is slow (messages could add up to a large amount of memory in the client process) -* When strict message ordering is necessary (the prefetch value should be set back to 1 in this case) -* Other special cases - -Also, with low-volume messaging and multiple consumers (including concurrency within a single listener container instance), you may wish to reduce the prefetch to get a more even distribution of messages across consumers. -We also recommend using `prefetch = 1` with the `MANUAL` `ack` mode. -The `basicAck` is an asynchronous operation and, if something wrong happens on the Broker (double `ack` for the same delivery tag, for example), you end up with processed subsequent messages in the batch that are unacknowledged on the Broker, and other consumers may see them. - -See <>. - -For more background about prefetch, see this post about https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/[consumer utilization in RabbitMQ] -and this post about https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/[queuing theory]. -==== - -====== Message Listener - -For asynchronous `Message` reception, a dedicated component (not the `AmqpTemplate`) is involved. -That component is a container for a `Message`-consuming callback. -We consider the container and its properties later in this section. -First, though, we should look at the callback, since that is where your application code is integrated with the messaging system. -There are a few options for the callback, starting with an implementation of the `MessageListener` interface, which the following listing shows: - -==== -[source,java] ----- -public interface MessageListener { - void onMessage(Message message); -} ----- -==== - -If your callback logic depends on the AMQP Channel instance for any reason, you may instead use the `ChannelAwareMessageListener`. -It looks similar but has an extra parameter. -The following listing shows the `ChannelAwareMessageListener` interface definition: - -==== -[source,java] ----- -public interface ChannelAwareMessageListener { - void onMessage(Message message, Channel channel) throws Exception; -} ----- -==== - -IMPORTANT: In version 2.1, this interface moved from package `o.s.amqp.rabbit.core` to `o.s.amqp.rabbit.listener.api`. - -[[message-listener-adapter]] -====== `MessageListenerAdapter` - -If you prefer to maintain a stricter separation between your application logic and the messaging API, you can rely upon an adapter implementation that is provided by the framework. -This is often referred to as "`Message-driven POJO`" support. - -NOTE: Version 1.5 introduced a more flexible mechanism for POJO messaging, the `@RabbitListener` annotation. -See <> for more information. - -When using the adapter, you need to provide only a reference to the instance that the adapter itself should invoke. -The following example shows how to do so: - -==== -[source,java] ----- -MessageListenerAdapter listener = new MessageListenerAdapter(somePojo); -listener.setDefaultListenerMethod("myMethod"); ----- -==== - -You can subclass the adapter and provide an implementation of `getListenerMethodName()` to dynamically select different methods based on the message. -This method has two parameters, `originalMessage` and `extractedMessage`, the latter being the result of any conversion. -By default, a `SimpleMessageConverter` is configured. -See <> for more information and information about other converters available. - -Starting with version 1.4.2, the original message has `consumerQueue` and `consumerTag` properties, which can be used to determine the queue from which a message was received. - -Starting with version 1.5, you can configure a map of consumer queue or tag to method name, to dynamically select the method to call. -If no entry is in the map, we fall back to the default listener method. -The default listener method (if not set) is `handleMessage`. - -Starting with version 2.0, a convenient `FunctionalInterface` has been provided. -The following listing shows the definition of `FunctionalInterface`: - -==== -[source, java] ----- -@FunctionalInterface -public interface ReplyingMessageListener { - - R handleMessage(T t); - -} ----- -==== - -This interface facilitates convenient configuration of the adapter by using Java 8 lambdas, as the following example shows: - -==== -[source, java] ----- -new MessageListenerAdapter((ReplyingMessageListener) data -> { - ... - return result; -})); ----- -==== - -Starting with version 2.2, the `buildListenerArguments(Object)` has been deprecated and new `buildListenerArguments(Object, Channel, Message)` one has been introduced instead. -The new method helps listener to get `Channel` and `Message` arguments to do more, such as calling `channel.basicReject(long, boolean)` in manual acknowledge mode. -The following listing shows the most basic example: - -==== -[source,java] ----- -public class ExtendedListenerAdapter extends MessageListenerAdapter { - - @Override - protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { - return new Object[]{extractedMessage, channel, message}; - } - -} ----- -==== - -Now you could configure `ExtendedListenerAdapter` as same as `MessageListenerAdapter` if you need to receive "`channel`" and "`message`". -Parameters of listener should be set as `buildListenerArguments(Object, Channel, Message)` returned, as the following example of listener shows: - -==== -[source,java] ----- -public void handleMessage(Object object, Channel channel, Message message) throws IOException { - ... -} ----- -==== - -====== Container - -Now that you have seen the various options for the `Message`-listening callback, we can turn our attention to the container. -Basically, the container handles the "`active`" responsibilities so that the listener callback can remain passive. -The container is an example of a "`lifecycle`" component. -It provides methods for starting and stopping. -When configuring the container, you essentially bridge the gap between an AMQP Queue and the `MessageListener` instance. -You must provide a reference to the `ConnectionFactory` and the queue names or Queue instances from which that listener should consume messages. - -Prior to version 2.0, there was one listener container, the `SimpleMessageListenerContainer`. -There is now a second container, the `DirectMessageListenerContainer`. -The differences between the containers and criteria you might apply when choosing which to use are described in <>. - -The following listing shows the most basic example, which works by using the, `SimpleMessageListenerContainer`: - -==== -[source,java] ----- -SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); -container.setConnectionFactory(rabbitConnectionFactory); -container.setQueueNames("some.queue"); -container.setMessageListener(new MessageListenerAdapter(somePojo)); ----- -==== - -As an "`active`" component, it is most common to create the listener container with a bean definition so that it can run in the background. -The following example shows one way to do so with XML: - -==== -[source,xml] ----- - - - ----- -==== - -The following listing shows another way to do so with XML: - -==== -[source,xml] ----- - - - ----- -==== - -Both of the preceding examples create a `DirectMessageListenerContainer` (notice the `type` attribute -- it defaults to `simple`). - -Alternately, you may prefer to use Java configuration, which looks similar to the preceding code snippet: - -==== -[source,java] ----- -@Configuration -public class ExampleAmqpConfiguration { - - @Bean - public SimpleMessageListenerContainer messageListenerContainer() { - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); - container.setConnectionFactory(rabbitConnectionFactory()); - container.setQueueName("some.queue"); - container.setMessageListener(exampleListener()); - return container; - } - - @Bean - public CachingConnectionFactory rabbitConnectionFactory() { - CachingConnectionFactory connectionFactory = - new CachingConnectionFactory("localhost"); - connectionFactory.setUsername("guest"); - connectionFactory.setPassword("guest"); - return connectionFactory; - } - - @Bean - public MessageListener exampleListener() { - return new MessageListener() { - public void onMessage(Message message) { - System.out.println("received: " + message); - } - }; - } -} ----- -==== - -[[consumer-priority]] -====== Consumer Priority - -Starting with RabbitMQ Version 3.2, the broker now supports consumer priority (see https://www.rabbitmq.com/blog/2013/12/16/using-consumer-priorities-with-rabbitmq/[Using Consumer Priorities with RabbitMQ]). -This is enabled by setting the `x-priority` argument on the consumer. -The `SimpleMessageListenerContainer` now supports setting consumer arguments, as the following example shows: - -==== -[source,java] ----- - -container.setConsumerArguments(Collections. - singletonMap("x-priority", Integer.valueOf(10))); ----- -==== - -For convenience, the namespace provides the `priority` attribute on the `listener` element, as the following example shows: - -==== -[source,xml] ----- - - - ----- -==== - -Starting with version 1.3, you can modify the queues on which the container listens at runtime. -See <>. - -[[lc-auto-delete]] -====== `auto-delete` Queues - -When a container is configured to listen to `auto-delete` queues, the queue has an `x-expires` option, or the https://www.rabbitmq.com/ttl.html[Time-To-Live] policy is configured on the Broker, the queue is removed by the broker when the container is stopped (that is, when the last consumer is cancelled). -Before version 1.3, the container could not be restarted because the queue was missing. -The `RabbitAdmin` only automatically redeclares queues and so on when the connection is closed or when it opens, which does not happen when the container is stopped and started. - -Starting with version 1.3, the container uses a `RabbitAdmin` to redeclare any missing queues during startup. - -You can also use conditional declaration (see <>) together with an `auto-startup="false"` admin to defer queue declaration until the container is started. -The following example shows how to do so: - -==== -[source,xml] ----- - - - - - - - - - - - - - ----- -==== - -In this case, the queue and exchange are declared by `containerAdmin`, which has `auto-startup="false"` so that the elements are not declared during context initialization. -Also, the container is not started for the same reason. -When the container is later started, it uses its reference to `containerAdmin` to declare the elements. - -[[de-batching]] -===== Batched Messages - -Batched messages (created by a producer) are automatically de-batched by listener containers (using the `springBatchFormat` message header). -Rejecting any message from a batch causes the entire batch to be rejected. -See <> for more information about batching. - -Starting with version 2.2, the `SimpleMessageListenerContainer` can be use to create batches on the consumer side (where the producer sent discrete messages). - -Set the container property `consumerBatchEnabled` to enable this feature. -`deBatchingEnabled` must also be true so that the container is responsible for processing batches of both types. -Implement `BatchMessageListener` or `ChannelAwareBatchMessageListener` when `consumerBatchEnabled` is true. -Starting with version 2.2.7 both the `SimpleMessageListenerContainer` and `DirectMessageListenerContainer` can debatch <> as `List`. -See <> for information about using this feature with `@RabbitListener`. - -[[consumer-events]] -===== Consumer Events - -The containers publish application events whenever a listener -(consumer) experiences a failure of some kind. -The event `ListenerContainerConsumerFailedEvent` has the following properties: - -* `container`: The listener container where the consumer experienced the problem. -* `reason`: A textual reason for the failure. -* `fatal`: A boolean indicating whether the failure was fatal. -With non-fatal exceptions, the container tries to restart the consumer, according to the `recoveryInterval` or `recoveryBackoff` (for the `SimpleMessageListenerContainer`) or the `monitorInterval` (for the `DirectMessageListenerContainer`). -* `throwable`: The `Throwable` that was caught. - -These events can be consumed by implementing `ApplicationListener`. - -NOTE: System-wide events (such as connection failures) are published by all consumers when `concurrentConsumers` is greater than 1. - -If a consumer fails because one if its queues is being used exclusively, by default, as well as publishing the event, a `WARN` log is issued. -To change this logging behavior, provide a custom `ConditionalExceptionLogger` in the `SimpleMessageListenerContainer` instance's `exclusiveConsumerExceptionLogger` property. -See also <>. - -Fatal errors are always logged at the `ERROR` level. -This it not modifiable. - -Several other events are published at various stages of the container lifecycle: - -* `AsyncConsumerStartedEvent`: When the consumer is started. -* `AsyncConsumerRestartedEvent`: When the consumer is restarted after a failure - `SimpleMessageListenerContainer` only. -* `AsyncConsumerTerminatedEvent`: When a consumer is stopped normally. -* `AsyncConsumerStoppedEvent`: When the consumer is stopped - `SimpleMessageListenerContainer` only. -* `ConsumeOkEvent`: When a `consumeOk` is received from the broker, contains the queue name and `consumerTag` -* `ListenerContainerIdleEvent`: See <>. -* `MissingQueueEvent`: When a missing queue is detected. - -[[consumerTags]] -===== Consumer Tags - -You can provide a strategy to generate consumer tags. -By default, the consumer tag is generated by the broker. -The following listing shows the `ConsumerTagStrategy` interface definition: - -==== -[source,java] ----- -public interface ConsumerTagStrategy { - - String createConsumerTag(String queue); - -} ----- -==== - -The queue is made available so that it can (optionally) be used in the tag. - -See <>. - -[[async-annotation-driven]] -===== Annotation-driven Listener Endpoints - -The easiest way to receive a message asynchronously is to use the annotated listener endpoint infrastructure. -In a nutshell, it lets you expose a method of a managed bean as a Rabbit listener endpoint. -The following example shows how to use the `@RabbitListener` annotation: - -==== -[source,java] ----- - -@Component -public class MyService { - - @RabbitListener(queues = "myQueue") - public void processOrder(String data) { - ... - } - -} ----- -==== - -The idea of the preceding example is that, whenever a message is available on the queue named `myQueue`, the `processOrder` method is invoked accordingly (in this case, with the payload of the message). - -The annotated endpoint infrastructure creates a message listener container behind the scenes for each annotated method, by using a `RabbitListenerContainerFactory`. - -In the preceding example, `myQueue` must already exist and be bound to some exchange. -The queue can be declared and bound automatically, as long as a `RabbitAdmin` exists in the application context. - -NOTE: Property placeholders (`${some.property}`) or SpEL expressions (`#{someExpression}`) can be specified for the annotation properties (`queues` etc). -See <> for an example of why you might use SpEL instead of a property placeholder. -The following listing shows three examples of how to declare a Rabbit listener: - -==== -[source,java] ----- - -@Component -public class MyService { - - @RabbitListener(bindings = @QueueBinding( - value = @Queue(value = "myQueue", durable = "true"), - exchange = @Exchange(value = "auto.exch", ignoreDeclarationExceptions = "true"), - key = "orderRoutingKey") - ) - public void processOrder(Order order) { - ... - } - - @RabbitListener(bindings = @QueueBinding( - value = @Queue, - exchange = @Exchange(value = "auto.exch"), - key = "invoiceRoutingKey") - ) - public void processInvoice(Invoice invoice) { - ... - } - - @RabbitListener(queuesToDeclare = @Queue(name = "${my.queue}", durable = "true")) - public String handleWithSimpleDeclare(String data) { - ... - } - -} ----- -==== - -In the first example, a queue `myQueue` is declared automatically (durable) together with the exchange, if needed, -and bound to the exchange with the routing key. -In the second example, an anonymous (exclusive, auto-delete) queue is declared and bound. -Multiple `QueueBinding` entries can be provided, letting the listener listen to multiple queues. -In the third example, a queue with the name retrieved from property `my.queue` is declared, if necessary, with the default binding to the default exchange using the queue name as the routing key. - -Since version 2.0, the `@Exchange` annotation supports any exchange types, including custom. -For more information, see https://www.rabbitmq.com/tutorials/amqp-concepts.html[AMQP Concepts]. - -You can use normal `@Bean` definitions when you need more advanced configuration. - -Notice `ignoreDeclarationExceptions` on the exchange in the first example. -This allows, for example, binding to an existing exchange that might have different settings (such as `internal`). -By default, the properties of an existing exchange must match. - -Starting with version 2.0, you can now bind a queue to an exchange with multiple routing keys, as the following example shows: - -==== -[source, java] ----- -... - key = { "red", "yellow" } -... ----- -==== - -You can also specify arguments within `@QueueBinding` annotations for queues, exchanges, -and bindings, as the following example shows: - -==== -[source, java] ----- -@RabbitListener(bindings = @QueueBinding( - value = @Queue(value = "auto.headers", autoDelete = "true", - arguments = @Argument(name = "x-message-ttl", value = "10000", - type = "java.lang.Integer")), - exchange = @Exchange(value = "auto.headers", type = ExchangeTypes.HEADERS, autoDelete = "true"), - arguments = { - @Argument(name = "x-match", value = "all"), - @Argument(name = "thing1", value = "somevalue"), - @Argument(name = "thing2") - }) -) -public String handleWithHeadersExchange(String foo) { - ... -} ----- -==== - -Notice that the `x-message-ttl` argument is set to 10 seconds for the queue. -Since the argument type is not `String`, we have to specify its type -- in this case, `Integer`. -As with all such declarations, if the queue already exists, the arguments must match those on the queue. -For the header exchange, we set the binding arguments to match messages that have the `thing1` header set to `somevalue`, and -the `thing2` header must be present with any value. -The `x-match` argument means both conditions must be satisfied. - -The argument name, value, and type can be property placeholders (`${...}`) or SpEL expressions (`#{...}`). -The `name` must resolve to a `String`. -The expression for `type` must resolve to a `Class` or the fully-qualified name of a class. -The `value` must resolve to something that can be converted by the `DefaultConversionService` to the type (such as the `x-message-ttl` in the preceding example). - -If a name resolves to `null` or an empty `String`, that `@Argument` is ignored. - -[[meta-annotation-driven]] -====== Meta-annotations - -Sometimes you may want to use the same configuration for multiple listeners. -To reduce the boilerplate configuration, you can use meta-annotations to create your own listener annotation. -The following example shows how to do so: - -==== -[source, java] ----- -@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@RabbitListener(bindings = @QueueBinding( - value = @Queue, - exchange = @Exchange(value = "metaFanout", type = ExchangeTypes.FANOUT))) -public @interface MyAnonFanoutListener { -} - -public class MetaListener { - - @MyAnonFanoutListener - public void handle1(String foo) { - ... - } - - @MyAnonFanoutListener - public void handle2(String foo) { - ... - } - -} ----- -==== - -In the preceding example, each listener created by the `@MyAnonFanoutListener` annotation binds an anonymous, auto-delete -queue to the fanout exchange, `metaFanout`. -Starting with version 2.2.3, `@AliasFor` is supported to allow overriding properties on the meta-annotated annotation. -Also, user annotations can now be `@Repeatable`, allowing multiple containers to be created for a method. - -==== -[source, java] ----- -@Component -static class MetaAnnotationTestBean { - - @MyListener("queue1") - @MyListener("queue2") - public void handleIt(String body) { - } - -} - - -@RabbitListener -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -@Repeatable(MyListeners.class) -static @interface MyListener { - - @AliasFor(annotation = RabbitListener.class, attribute = "queues") - String[] value() default {}; - -} - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -static @interface MyListeners { - - MyListener[] value(); - -} ----- -==== - - -[[async-annotation-driven-enable]] -====== Enable Listener Endpoint Annotations - -To enable support for `@RabbitListener` annotations, you can add `@EnableRabbit` to one of your `@Configuration` classes. -The following example shows how to do so: - -==== -[source,java] ----- -@Configuration -@EnableRabbit -public class AppConfig { - - @Bean - public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory()); - factory.setConcurrentConsumers(3); - factory.setMaxConcurrentConsumers(10); - factory.setContainerCustomizer(container -> /* customize the container */); - return factory; - } -} ----- -==== - -Since version 2.0, a `DirectMessageListenerContainerFactory` is also available. -It creates `DirectMessageListenerContainer` instances. - -NOTE: For information to help you choose between `SimpleRabbitListenerContainerFactory` and `DirectRabbitListenerContainerFactory`, see <>. - -Starting wih version 2.2.2, you can provide a `ContainerCustomizer` implementation (as shown above). -This can be used to further configure the container after it has been created and configured; you can use this, for example, to set properties that are not exposed by the container factory. - -By default, the infrastructure looks for a bean named `rabbitListenerContainerFactory` as the source for the factory to use to create message listener containers. -In this case, and ignoring the RabbitMQ infrastructure setup, the `processOrder` method can be invoked with a core poll 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 `RabbitListenerConfigurer` interface. -The default is required only if at least one endpoint is registered without a specific container factory. -See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/annotation/RabbitListenerConfigurer.html[Javadoc] for full details and examples. - -The container factories provide methods for adding `MessagePostProcessor` instances that are applied after receiving messages (before invoking the listener) and before sending replies. - -See <> for information about replies. - -Starting with version 2.0.6, you can add a `RetryTemplate` and `RecoveryCallback` to the listener container factory. -It is used when sending replies. -The `RecoveryCallback` is invoked when retries are exhausted. -You can use a `SendRetryContextAccessor` to get information from the context. -The following example shows how to do so: - -==== -[source, java] ----- -factory.setRetryTemplate(retryTemplate); -factory.setReplyRecoveryCallback(ctx -> { - Message failed = SendRetryContextAccessor.getMessage(ctx); - Address replyTo = SendRetryContextAccessor.getAddress(ctx); - Throwable t = ctx.getLastThrowable(); - ... - return null; -}); ----- -==== - -If you prefer XML configuration, you can use the `` element. -Any beans annotated with `@RabbitListener` are detected. - -For `SimpleRabbitListenerContainer` instances, you can use XML similar to the following: - -==== -[source,xml] ----- - - - - - - - ----- -==== - -For `DirectMessageListenerContainer` instances, you can use XML similar to the following: - -==== -[source,xml] ----- - - - - - - ----- -==== - -[[listener-property-overrides]] - -Starting with version 2.0, the `@RabbitListener` annotation has a `concurrency` property. -It supports SpEL expressions (`#{...}`) and property placeholders (`${...}`). -Its meaning and allowed values depend on the container type, as follows: - -* For the `DirectMessageListenerContainer`, the value must be a single integer value, which sets the `consumersPerQueue` property on the container. -* For the `SimpleRabbitListenerContainer`, the value can be a single integer value, which sets the `concurrentConsumers` property on the container, or it can have the form, `m-n`, where `m` is the `concurrentConsumers` property and `n` is the `maxConcurrentConsumers` property. - -In either case, this setting overrides the settings on the factory. -Previously you had to define different container factories if you had listeners that required different concurrency. - -The annotation also allows overriding the factory `autoStartup` and `taskExecutor` properties via the `autoStartup` and `executor` (since 2.2) annotation properties. -Using a different executor for each might help with identifying threads associated with each listener in logs and thread dumps. - -Version 2.2 also added the `ackMode` property, which allows you to override the container factory's `acknowledgeMode` property. - -==== -[source, java] ----- -@RabbitListener(id = "manual.acks.1", queues = "manual.acks.1", ackMode = "MANUAL") -public void manual1(String in, Channel channel, - @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException { - - ... - channel.basicAck(tag, false); -} ----- -==== - -[[async-annotation-conversion]] -====== Message Conversion for Annotated Methods - -There are two conversion steps in the pipeline before invoking the listener. -The first step uses a `MessageConverter` to convert the incoming Spring AMQP `Message` to a Spring-messaging `Message`. -When the target method is invoked, the message payload is converted, if necessary, to the method parameter type. - -The default `MessageConverter` for the first step is a Spring AMQP `SimpleMessageConverter` that handles conversion to -`String` and `java.io.Serializable` objects. -All others remain as a `byte[]`. -In the following discussion, we call this the "`message converter`". - -The default converter for the second step is a `GenericMessageConverter`, which delegates to a conversion service -(an instance of `DefaultFormattingConversionService`). -In the following discussion, we call this the "`method argument converter`". - -To change the message converter, you can add it as a property to the container factory bean. -The following example shows how to do so: - -==== -[source, java] ----- -@Bean -public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - ... - factory.setMessageConverter(new Jackson2JsonMessageConverter()); - ... - return factory; -} ----- -==== - -This configures a Jackson2 converter that expects header information to be present to guide the conversion. - -You can also use a `ContentTypeDelegatingMessageConverter`, which can handle conversion of different content types. - -Starting with version 2.3, you can override the factory converter by specifying a bean name in the `messageConverter` property. - -==== -[source, java] ----- -@Bean -public Jackson2JsonMessageConverter jsonConverter() { - return new Jackson2JsonMessageConverter(); -} - -@RabbitListener(..., messageConverter = "jsonConverter") -public void listen(String in) { - ... -} ----- -==== - -This avoids having to declare a different container factory just to change the converter. - -In most cases, it is not necessary to customize the method argument converter unless, for example, you want to use -a custom `ConversionService`. - -In versions prior to 1.6, the type information to convert the JSON had to be provided in message headers, or a -custom `ClassMapper` was required. -Starting with version 1.6, if there are no type information headers, the type can be inferred from the target -method arguments. - -NOTE: This type inference works only for `@RabbitListener` at the method level. - -See <> for more information. - -If you wish to customize the method argument converter, you can do so as follows: - -==== -[source, java] ----- -@Configuration -@EnableRabbit -public class AppConfig implements RabbitListenerConfigurer { - - ... - - @Bean - public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() { - DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory(); - factory.setMessageConverter(new GenericMessageConverter(myConversionService())); - return factory; - } - - @Bean - public DefaultConversionService myConversionService() { - DefaultConversionService conv = new DefaultConversionService(); - conv.addConverter(mySpecialConverter()); - return conv; - } - - @Override - public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { - registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory()); - } - - ... - -} ----- -==== - -IMPORTANT: For multi-method listeners (see <>), the method selection is based on the payload of the message *after the message conversion*. -The method argument converter is called only after the method has been selected. - -[[custom-argument-resolver]] -====== Adding a Custom `HandlerMethodArgumentResolver` to @RabbitListener - -Starting with version 2.3.7 you are able to add your own `HandlerMethodArgumentResolver` and resolve custom method parameters. -All you need is to implement `RabbitListenerConfigurer` and use method `setCustomMethodArgumentResolvers()` from class `RabbitListenerEndpointRegistrar`. - -==== -[source, java] ----- -@Configuration -class CustomRabbitConfig implements RabbitListenerConfigurer { - - @Override - public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { - registrar.setCustomMethodArgumentResolvers( - new HandlerMethodArgumentResolver() { - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return CustomMethodArgument.class.isAssignableFrom(parameter.getParameterType()); - } - - @Override - public Object resolveArgument(MethodParameter parameter, org.springframework.messaging.Message message) { - return new CustomMethodArgument( - (String) message.getPayload(), - message.getHeaders().get("customHeader", String.class) - ); - } - - } - ); - } - -} ----- -==== - -[[async-annotation-driven-registration]] -====== Programmatic Endpoint Registration - -`RabbitListenerEndpoint` provides a model of a Rabbit endpoint and is responsible for configuring the container for that model. -The infrastructure lets you configure endpoints programmatically in addition to the ones that are detected by the `RabbitListener` annotation. -The following example shows how to do so: - -==== -[source,java] ----- -@Configuration -@EnableRabbit -public class AppConfig implements RabbitListenerConfigurer { - - @Override - public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { - SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); - endpoint.setQueueNames("anotherQueue"); - endpoint.setMessageListener(message -> { - // processing - }); - registrar.registerEndpoint(endpoint); - } -} ----- -==== - -In the preceding example, we used `SimpleRabbitListenerEndpoint`, which provides the actual `MessageListener` to invoke, but you could just as well build your own endpoint variant to describe a custom invocation mechanism. - -It should be noted that you could just as well skip the use of `@RabbitListener` altogether and register your endpoints programmatically through `RabbitListenerConfigurer`. - -[[async-annotation-driven-enable-signature]] -====== Annotated Endpoint Method Signature - -So far, we have been injecting a simple `String` in our endpoint, but it can actually have a very flexible method signature. -The following example rewrites it to inject the `Order` with a custom header: - -==== -[source,java] ----- -@Component -public class MyService { - - @RabbitListener(queues = "myQueue") - public void processOrder(Order order, @Header("order_type") String orderType) { - ... - } -} ----- -==== - -The following list shows the arguments that are available to be matched with parameters in listener endpoints: - -* The raw `org.springframework.amqp.core.Message`. -* The `MessageProperties` from the raw `Message`. -* The `com.rabbitmq.client.Channel` on which the message was received. -* The `org.springframework.messaging.Message` converted from the incoming AMQP message. -* `@Header`-annotated method arguments to extract a specific header value, including standard AMQP headers. -* `@Headers`-annotated argument that must also be assignable to `java.util.Map` for getting access to all headers. -* The converted payload - -A non-annotated element that is not one of the supported types (that is, -`Message`, `MessageProperties`, `Message` and `Channel`) is matched with the payload. -You can make that explicit by annotating the parameter with `@Payload`. -You can also turn on validation by adding an extra `@Valid`. - -The ability to inject Spring’s message abstraction is particularly useful to benefit from all the information stored in the transport-specific message without relying on the transport-specific API. -The following example shows how to do so: - -==== -[source,java] ----- - -@RabbitListener(queues = "myQueue") -public void processOrder(Message order) { ... -} - ----- -==== - -Handling of method arguments is provided by `DefaultMessageHandlerMethodFactory`, which you can further customize to support additional method arguments. -The conversion and validation support can be customized there as well. - -For instance, if we want to make sure our `Order` is valid before processing it, we can annotate the payload with `@Valid` and configure the necessary validator, as follows: - -==== -[source,java] ----- - -@Configuration -@EnableRabbit -public class AppConfig implements RabbitListenerConfigurer { - - @Override - public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { - registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory()); - } - - @Bean - public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() { - DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory(); - factory.setValidator(myValidator()); - return factory; - } -} ----- -==== - -[[rabbit-validation]] -====== @RabbitListener @Payload Validation - -Starting with version 2.3.7, it is now easier to add a `Validator` to validate `@RabbitListener` and `@RabbitHandler` `@Payload` arguments. -Now, you can simply add the validator to the registrar itself. - -==== -[source, java] ----- -@Configuration -@EnableRabbit -public class Config implements RabbitListenerConfigurer { - ... - @Override - public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { - registrar.setValidator(new MyValidator()); - } -} ----- -==== - -NOTE: When using Spring Boot with the validation starter, a `LocalValidatorFactoryBean` is auto-configured: - -==== -[source, java] ----- -@Configuration -@EnableRabbit -public class Config implements RabbitListenerConfigurer { - @Autowired - private LocalValidatorFactoryBean validator; - ... - @Override - public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { - registrar.setValidator(this.validator); - } -} ----- -==== - -To validate: - -==== -[source, java] ----- -public static class ValidatedClass { - @Max(10) - private int bar; - public int getBar() { - return this.bar; - } - public void setBar(int bar) { - this.bar = bar; - } -} ----- -==== - -and - -==== -[source, java] ----- -@RabbitListener(id="validated", queues = "queue1", errorHandler = "validationErrorHandler", - containerFactory = "jsonListenerContainerFactory") -public void validatedListener(@Payload @Valid ValidatedClass val) { - ... -} -@Bean -public RabbitListenerErrorHandler validationErrorHandler() { - return (m, e) -> { - ... - }; -} ----- -==== - -[[annotation-multiple-queues]] -====== Listening to Multiple Queues - -When you use the `queues` attribute, you can specify that the associated container can listen to multiple queues. -You can use a `@Header` annotation to make the queue name from which a message was received available to the POJO -method. -The following example shows how to do so: - -==== -[source, java] ----- -@Component -public class MyService { - - @RabbitListener(queues = { "queue1", "queue2" } ) - public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { - ... - } - -} ----- -==== - -Starting with version 1.5, you can externalize the queue names by using property placeholders and SpEL. -The following example shows how to do so: - -==== -[source, java] ----- -@Component -public class MyService { - - @RabbitListener(queues = "#{'${property.with.comma.delimited.queue.names}'.split(',')}" ) - public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { - ... - } - -} ----- -==== - -Prior to version 1.5, only a single queue could be specified this way. -Each queue needed a separate property. - -[[async-annotation-driven-reply]] -====== Reply Management - -The existing support in `MessageListenerAdapter` already lets your method have a non-void return type. -When that is the case, the result of the invocation is encapsulated in a message sent to the the address specified in the `ReplyToAddress` header of the original message, or to the default address configured on the listener. -You can set that default address by using the `@SendTo` annotation of the messaging abstraction. - -Assuming our `processOrder` method should now return an `OrderStatus`, we can write it as follows to automatically send a reply: - -==== -[source,java] ----- -@RabbitListener(destination = "myQueue") -@SendTo("status") -public OrderStatus processOrder(Order order) { - // order processing - return status; -} ----- -==== - -If you need to set additional headers in a transport-independent manner, you could return a `Message` instead, something like the following: - -==== -[source,java] ----- - -@RabbitListener(destination = "myQueue") -@SendTo("status") -public Message processOrder(Order order) { - // order processing - return MessageBuilder - .withPayload(status) - .setHeader("code", 1234) - .build(); -} ----- -==== - -Alternatively, you can use a `MessagePostProcessor` in the `beforeSendReplyMessagePostProcessors` container factory property to add more headers. -Starting with version 2.2.3, the called bean/method is made avaiable in the reply message, which can be used in a message post processor to communicate the information back to the caller: - -==== -[source, java] ----- -factory.setBeforeSendReplyPostProcessors(msg -> { - msg.getMessageProperties().setHeader("calledBean", - msg.getMessageProperties().getTargetBean().getClass().getSimpleName()); - msg.getMessageProperties().setHeader("calledMethod", - msg.getMessageProperties().getTargetMethod().getName()); - return m; -}); ----- -==== - -Starting with version 2.2.5, you can configure a `ReplyPostProcessor` to modify the reply message before it is sent; it is called after the `correlationId` header has been set up to match the request. - -==== -[source, java] ----- -@RabbitListener(queues = "test.header", group = "testGroup", replyPostProcessor = "echoCustomHeader") -public String capitalizeWithHeader(String in) { - return in.toUpperCase(); -} - -@Bean -public ReplyPostProcessor echoCustomHeader() { - return (req, resp) -> { - resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader")); - return resp; - }; -} ----- -==== - -The `@SendTo` value is assumed as a reply `exchange` and `routingKey` pair that follows the `exchange/routingKey` pattern, -where one of those parts can be omitted. -The valid values are as follows: - -* `thing1/thing2`: The `replyTo` exchange and the `routingKey`. -`thing1/`: The `replyTo` exchange and the default (empty) `routingKey`. -`thing2` or `/thing2`: The `replyTo` `routingKey` and the default (empty) exchange. -`/` or empty: The `replyTo` default exchange and the default `routingKey`. - -Also, you can use `@SendTo` without a `value` attribute. -This case is equal to an empty `sendTo` pattern. -`@SendTo` is used only if the inbound message does not have a `replyToAddress` property. - -Starting with version 1.5, the `@SendTo` value can be a bean initialization SpEL Expression, as shown in the following example: - -==== -[source, java] ----- -@RabbitListener(queues = "test.sendTo.spel") -@SendTo("#{spelReplyTo}") -public String capitalizeWithSendToSpel(String foo) { - return foo.toUpperCase(); -} -... -@Bean -public String spelReplyTo() { - return "test.sendTo.reply.spel"; -} ----- -==== - -The expression must evaluate to a `String`, which can be a simple queue name (sent to the default exchange) or with -the form `exchange/routingKey` as discussed prior to the preceding example. - -NOTE: The `#{...}` expression is evaluated once, during initialization. - -For dynamic reply routing, the message sender should include a `reply_to` message property or use the alternate -runtime SpEL expression (described after the next example). - -Starting with version 1.6, the `@SendTo` can be a SpEL expression that is evaluated at runtime against the request -and reply, as the following example shows: - -==== -[source, java] ----- -@RabbitListener(queues = "test.sendTo.spel") -@SendTo("!{'some.reply.queue.with.' + result.queueName}") -public Bar capitalizeWithSendToSpel(Foo foo) { - return processTheFooAndReturnABar(foo); -} ----- -==== - -The runtime nature of the SpEL expression is indicated with `!{...}` delimiters. -The evaluation context `#root` object for the expression has three properties: - -* `request`: The `o.s.amqp.core.Message` request object. -* `source`: The `o.s.messaging.Message` after conversion. -* `result`: The method result. - -The context has a map property accessor, a standard type converter, and a bean resolver, which lets other beans in the -context be referenced (for example, `@someBeanName.determineReplyQ(request, result)`). - -In summary, `#{...}` is evaluated once during initialization, with the `#root` object being the application context. -Beans are referenced by their names. -`!{...}` is evaluated at runtime for each message, with the root object having the properties listed earlier. -Beans are referenced with their names, prefixed by `@`. - -Starting with version 2.1, simple property placeholders are also supported (for example, `${some.reply.to}`). -With earlier versions, the following can be used as a work around, as the following example shows: - -==== -[source, java] ----- -@RabbitListener(queues = "foo") -@SendTo("#{environment['my.send.to']}") -public String listen(Message in) { - ... - return ... -} ----- -==== - -[[reply-content-type]] -====== Reply ContentType - -If you are using a sophisticated message converter, such as the `ContentTypeDelegatingMessageConverter`, you can control the content type of the reply by setting the `replyContentType` property on the listener. -This allows the converter to select the appropriate delegate converter for the reply. - -==== -[source, java] ----- -@RabbitListener(queues = "q1", messageConverter = "delegating", - replyContentType = "application/json") -public Thing2 listen(Thing1 in) { - ... -} ----- -==== - -By default, for backwards compatibility, any content type property set by the converter will be overwritten by this value after conversion. -Converters such as the `SimpleMessageConverter` use the reply type rather than the content type to determine the conversion needed and sets the content type in the reply message appropriately. -This may not be the desired action and can be overridden by setting the `converterWinsContentType` property to `false`. -For example, if you return a `String` containing JSON, the `SimpleMessageConverter` will set the content type in the reply to `text/plain`. -The following configuration will ensure the content type is set properly, even if the `SimpleMessageConverter` is used. - -==== -[source, java] ----- -@RabbitListener(queues = "q1", replyContentType = "application/json", - converterWinsContentType = "false") -public String listen(Thing in) { - ... - return someJsonString; -} ----- -==== - -These properties (`replyContentType` and `converterWinsContentType`) do not apply when the return type is a Spring AMQP `Message` or a Spring Messaging `Message`. -In the first case, there is no conversion involved; simply set the `contentType` message property. -In the second case, the behavior is controlled using message headers: - -==== -[source, java] ----- -@RabbitListener(queues = "q1", messageConverter = "delegating") -@SendTo("q2") -public Message listen(String in) { - ... - return MessageBuilder.withPayload(in.toUpperCase()) - .setHeader(MessageHeaders.CONTENT_TYPE, "application/xml") - .build(); -} ----- -==== - -This content type will be passed in the `MessageProperties` to the converter. -By default, for backwards compatibility, any content type property set by the converter will be overwritten by this value after conversion. -If you wish to override that behavior, also set the `AmqpHeaders.CONTENT_TYPE_CONVERTER_WINS` to `true` and any value set by the converter will be retained. - -[[annotation-method-selection]] -====== Multi-method Listeners - -Starting with version 1.5.0, you can specify the `@RabbitListener` annotation at the class level. -Together with the new `@RabbitHandler` annotation, this lets a single listener invoke different methods, based on -the payload type of the incoming message. -This is best described using an example: - -==== -[source, java] ----- -@RabbitListener(id="multi", queues = "someQueue") -@SendTo("my.reply.queue") -public class MultiListenerBean { - - @RabbitHandler - public String thing2(Thing2 thing2) { - ... - } - - @RabbitHandler - public String cat(Cat cat) { - ... - } - - @RabbitHandler - public String hat(@Header("amqp_receivedRoutingKey") String rk, @Payload Hat hat) { - ... - } - - @RabbitHandler(isDefault = true) - public String defaultMethod(Object object) { - ... - } - -} ----- -==== - -In this case, the individual `@RabbitHandler` methods are invoked if the converted payload is a `Thing2`, a `Cat`, or a `Hat`. -You should understand that the system must be able to identify a unique method based on the payload type. -The type is checked for assignability to a single parameter that has no annotations or that is annotated with the `@Payload` annotation. -Notice that the same method signatures apply, as discussed in the method-level `@RabbitListener` (<>). - -Starting with version 2.0.3, a `@RabbitHandler` method can be designated as the default method, which is invoked if there is no match on other methods. -At most, one method can be so designated. - -IMPORTANT: `@RabbitHandler` is intended only for processing message payloads after conversion, if you wish to receive the unconverted raw `Message` object, you must use `@RabbitListener` on the method, not the class. - -[[repeatable-rabbit-listener]] -====== `@Repeatable` `@RabbitListener` - -Starting with version 1.6, the `@RabbitListener` annotation is marked with `@Repeatable`. -This means that the annotation can appear on the same annotated element (method or class) multiple times. -In this case, a separate listener container is created for each annotation, each of which invokes the same listener -`@Bean`. -Repeatable annotations can be used with Java 8 or above. - -====== Proxy `@RabbitListener` and Generics - -If your service is intended to be proxied (for example, in the case of `@Transactional`), you should keep in mind some considerations when -the interface has generic parameters. -Consider the following example: - -==== -[source, java] ----- -interface TxService

{ - - String handle(P payload, String header); - -} - -static class TxServiceImpl implements TxService { - - @Override - @RabbitListener(...) - public String handle(Thing thing, String rk) { - ... - } - -} ----- -==== - -With a generic interface and a particular implementation, you are forced to switch to the CGLIB target class proxy because the actual implementation of the interface -`handle` method is a bridge method. -In the case of transaction management, the use of CGLIB is configured by using -an annotation option: `@EnableTransactionManagement(proxyTargetClass = true)`. -And in this case, all annotations have to be declared on the target method in the implementation, as the following example shows: - -==== -[source, java] ----- -static class TxServiceImpl implements TxService { - - @Override - @Transactional - @RabbitListener(...) - public String handle(@Payload Foo foo, @Header("amqp_receivedRoutingKey") String rk) { - ... - } - -} ----- -==== - -[[annotation-error-handling]] -====== Handling Exceptions - -By default, if an annotated listener method throws an exception, it is thrown to the container and the message are requeued and redelivered, discarded, or routed to a dead letter exchange, depending on the container and broker configuration. -Nothing is returned to the sender. - -Starting with version 2.0, the `@RabbitListener` annotation has two new attributes: `errorHandler` and `returnExceptions`. - -These are not configured by default. - -You can use the `errorHandler` to provide the bean name of a `RabbitListenerErrorHandler` implementation. -This functional interface has one method, as follows: - -[source, java] ----- -@FunctionalInterface -public interface RabbitListenerErrorHandler { - - Object handleError(Message amqpMessage, org.springframework.messaging.Message message, - ListenerExecutionFailedException exception) throws Exception; - -} ----- - -As you can see, you have access to the raw message received from the container, the spring-messaging `Message` object produced by the message converter, and the exception that was thrown by the listener (wrapped in a `ListenerExecutionFailedException`). -The error handler can either return some result (which is sent as the reply) or throw the original or a new exception (which is thrown to the container or returned to the sender, depending on the `returnExceptions` setting). - -The `returnExceptions` attribute, when `true`, causes exceptions to be returned to the sender. -The exception is wrapped in a `RemoteInvocationResult` object. -On the sender side, there is an available `RemoteInvocationAwareMessageConverterAdapter`, which, if configured into the `RabbitTemplate`, re-throws the server-side exception, wrapped in an `AmqpRemoteException`. -The stack trace of the server exception is synthesized by merging the server and client stack traces. - -IMPORTANT: This mechanism generally works only with the default `SimpleMessageConverter`, which uses Java serialization. -Exceptions are generally not "`Jackson-friendly`" and cannot be serialized to JSON. -If you use JSON, consider using an `errorHandler` to return some other Jackson-friendly `Error` object when an exception is thrown. - -IMPORTANT: In version 2.1, this interface moved from package `o.s.amqp.rabbit.listener` to `o.s.amqp.rabbit.listener.api`. - -Starting with version 2.1.7, the `Channel` is available in a messaging message header; this allows you to ack or nack the failed messasge when using `AcknowledgeMode.MANUAL`: - -==== -[source, java] ----- -public Object handleError(Message amqpMessage, org.springframework.messaging.Message message, - ListenerExecutionFailedException exception) { - ... - message.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class) - .basicReject(message.getHeaders().get(AmqpHeaders.DELIVERY_TAG, Long.class), - true); - } ----- -==== - -Starting with version 2.2.18, if a message conversion exception is thrown, the error handler will be called, with `null` in the `message` argument. -This allows the application to send some result to the caller, indicating that a badly-formed message was received. -Previously, such errors were thrown and handled by the container. - -====== Container Management - -Containers created for annotations are not registered with the application context. -You can obtain a collection of all containers by invoking `getListenerContainers()` on the -`RabbitListenerEndpointRegistry` bean. -You can then iterate over this collection, for example, to stop or start all containers or invoke the `Lifecycle` methods -on the registry itself, which will invoke the operations on each container. - -You can also get a reference to an individual container by using its `id`, using `getListenerContainer(String id)` -- for -example, `registry.getListenerContainer("multi")` for the container created by the snippet above. - -Starting with version 1.5.2, you can obtain the `id` values of the registered containers with `getListenerContainerIds()`. - -Starting with version 1.5, you can now assign a `group` to the container on the `RabbitListener` endpoint. -This provides a mechanism to get a reference to a subset of containers. -Adding a `group` attribute causes a bean of type `Collection` to be registered with the context with the group name. - -[[receiving-batch]] -===== @RabbitListener with Batching - -When receiving a <> of messages, the de-batching is normally performed by the container and the listener is invoked with one message at at time. -Starting with version 2.2, you can configure the listener container factory and listener to receive the entire batch in one call, simply set the factory's `batchListener` property, and make the method payload parameter a `List`: - -==== -[source, java] ----- -@Bean -public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory()); - factory.setBatchListener(true); - return factory; -} - -@RabbitListener(queues = "batch.1") -public void listen1(List in) { - ... -} - -// or - -@RabbitListener(queues = "batch.2") -public void listen2(List> in) { - ... -} ----- -==== - -Setting the `batchListener` property to true automatically turns off the `deBatchingEnabled` container property in containers that the factory creates (unless `consumerBatchEnabled` is `true` - see below). Effectively, the debatching is moved from the container to the listener adapter and the adapter creates the list that is passed to the listener. - -A batch-enabled factory cannot be used with a <>. - -Also starting with version 2.2. when receiving batched messages one-at-a-time, the last message contains a boolean header set to `true`. -This header can be obtained by adding the `@Header(AmqpHeaders.LAST_IN_BATCH)` boolean last` parameter to your listener method. -The header is mapped from `MessageProperties.isLastInBatch()`. -In addition, `AmqpHeaders.BATCH_SIZE` is populated with the size of the batch in every message fragment. - -In addition, a new property `consumerBatchEnabled` has been added to the `SimpleMessageListenerContainer`. -When this is true, the container will create a batch of messages, up to `batchSize`; a partial batch is delivered if `receiveTimeout` elapses with no new messages arriving. -If a producer-created batch is received, it is debatched and added to the consumer-side batch; therefore the actual number of messages delivered may exceed `batchSize`, which represents the number of messages received from the broker. -`deBatchingEnabled` must be true when `consumerBatchEnabled` is true; the container factory will enforce this requirement. - -==== -[source, java] ----- -@Bean -public SimpleRabbitListenerContainerFactory consumerBatchContainerFactory() { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(rabbitConnectionFactory()); - factory.setConsumerTagStrategy(consumerTagStrategy()); - factory.setBatchListener(true); // configures a BatchMessageListenerAdapter - factory.setBatchSize(2); - factory.setConsumerBatchEnabled(true); - return factory; -} ----- -==== - -When using `consumerBatchEnabled` with `@RabbitListener`: - -==== -[source, java] ----- -@RabbitListener(queues = "batch.1", containerFactory = "consumerBatchContainerFactory") -public void consumerBatch1(List amqpMessages) { - this.amqpMessagesReceived = amqpMessages; - this.batch1Latch.countDown(); -} - -@RabbitListener(queues = "batch.2", containerFactory = "consumerBatchContainerFactory") -public void consumerBatch2(List> messages) { - this.messagingMessagesReceived = messages; - this.batch2Latch.countDown(); -} - -@RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory") -public void consumerBatch3(List strings) { - this.batch3Strings = strings; - this.batch3Latch.countDown(); -} ----- -==== - -* the first is called with the raw, unconverted `org.springframework.amqp.core.Message` s received. -* the second is called with the `org.springframework.messaging.Message` s with converted payloads and mapped headers/properties. -* the third is called with the converted payloads, with no access to headers/properteis. - -You can also add a `Channel` parameter, often used when using `MANUAL` ack mode. -This is not very useful with the third example because you don't have access to the `delivery_tag` property. - -[[using-container-factories]] -===== Using Container Factories - -Listener container factories were introduced to support the `@RabbitListener` and registering containers with the `RabbitListenerEndpointRegistry`, as discussed in <>. - -Starting with version 2.1, they can be used to create any listener container -- even a container without a listener (such as for use in Spring Integration). -Of course, a listener must be added before the container is started. - -There are two ways to create such containers: - -* Use a SimpleRabbitListenerEndpoint -* Add the listener after creation - -The following example shows how to use a `SimpleRabbitListenerEndpoint` to create a listener container: - -==== -[source, java] ----- -@Bean -public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener( - SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { - SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); - endpoint.setQueueNames("queue.1"); - endpoint.setMessageListener(message -> { - ... - }); - return rabbitListenerContainerFactory.createListenerContainer(endpoint); -} ----- -==== - -The following example shows how to add the listener after creation: - -==== -[source, java] ----- -@Bean -public SimpleMessageListenerContainer factoryCreatedContainerNoListener( - SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { - SimpleMessageListenerContainer container = rabbitListenerContainerFactory.createListenerContainer(); - container.setMessageListener(message -> { - ... - }); - container.setQueueNames("test.no.listener.yet"); - return container; -} ----- -==== - -In either case, the listener can also be a `ChannelAwareMessageListener`, since it is now a sub-interface of `MessageListener`. - -These techniques are useful if you wish to create several containers with similar properties or use a pre-configured container factory such as the one provided by Spring Boot auto configuration or both. - -IMPORTANT: Containers created this way are normal `@Bean` instances and are not registered in the `RabbitListenerEndpointRegistry`. - -[[async-returns]] -===== Asynchronous `@RabbitListener` Return Types - -Starting with version 2.1, `@RabbitListener` (and `@RabbitHandler`) methods can be specified with asynchronous return types `ListenableFuture` and `Mono`, letting the reply be sent asynchronously. - -IMPORTANT: The listener container factory must be configured with `AcknowledgeMode.MANUAL` so that the consumer thread will not ack the message; instead, the asynchronous completion will ack or nack the message when the async operation completes. -When the async result is completed with an error, whether the message is requeued or not depends on the exception type thrown, the container configuration, and the container error handler. -By default, the message will be requeued, unless the container's `defaultRequeueRejected` property is set to `false` (it is `true` by default). -If the async result is completed with an `AmqpRejectAndDontRequeueException`, the message will not be requeued. -If the container's `defaultRequeueRejected` property is `false`, you can override that by setting the future's exception to a `ImmediateRequeueException` and the message will be requeued. -If some exception occurs within the listener method that prevents creation of the async result object, you MUST catch that exception and return an appropriate return object that will cause the message to be acknowledged or requeued. - -[[threading]] -===== Threading and Asynchronous Consumers - -A number of different threads are involved with asynchronous consumers. - -Threads from the `TaskExecutor` configured in the `SimpleMessageListenerContainer` are used to invoke the `MessageListener` when a new message is delivered by `RabbitMQ Client`. -If not configured, a `SimpleAsyncTaskExecutor` is used. -If you use a pooled executor, you need to ensure the pool size is sufficient to handle the configured concurrency. -With the `DirectMessageListenerContainer`, the `MessageListener` is invoked directly on a `RabbitMQ Client` thread. -In this case, the `taskExecutor` is used for the task that monitors the consumers. - -NOTE: When using the default `SimpleAsyncTaskExecutor`, for the threads the listener is invoked on, the listener container `beanName` is used in the `threadNamePrefix`. -This is useful for log analysis. -We generally recommend always including the thread name in the logging appender configuration. -When a `TaskExecutor` is specifically provided through the `taskExecutor` property on the container, it is used as is, without modification. -It is recommended that you use a similar technique to name the threads created by a custom `TaskExecutor` bean definition, to aid with thread identification in log messages. - -The `Executor` configured in the `CachingConnectionFactory` is passed into the `RabbitMQ Client` when creating the connection, and its threads are used to deliver new messages to the listener container. -If this is not configured, the client uses an internal thread pool executor with (at the time of writing) a pool size of `Runtime.getRuntime().availableProcessors() * 2` for each connection. - -If you have a large number of factories or are using `CacheMode.CONNECTION`, you may wish to consider using a shared `ThreadPoolTaskExecutor` with enough threads to satisfy your workload. - -IMPORTANT: With the `DirectMessageListenerContainer`, you need to ensure that the connection factory is configured with a task executor that has sufficient threads to support your desired concurrency across all listener containers that use that factory. -The default pool size (at the time of writing) is `Runtime.getRuntime().availableProcessors() * 2`. - -The `RabbitMQ client` uses a `ThreadFactory` to create threads for low-level I/O (socket) operations. -To modify this factory, you need to configure the underlying RabbitMQ `ConnectionFactory`, as discussed in <>. - -[[choose-container]] -===== Choosing a Container - -Version 2.0 introduced the `DirectMessageListenerContainer` (DMLC). -Previously, only the `SimpleMessageListenerContainer` (SMLC) was available. -The SMLC uses an internal queue and a dedicated thread for each consumer. -If a container is configured to listen to multiple queues, the same consumer thread is used to process all the queues. -Concurrency is controlled by `concurrentConsumers` and other properties. -As messages arrive from the RabbitMQ client, the client thread hands them off to the consumer thread through the queue. -This architecture was required because, in early versions of the RabbitMQ client, multiple concurrent deliveries were not possible. -Newer versions of the client have a revised threading model and can now support concurrency. -This has allowed the introduction of the DMLC where the listener is now invoked directly on the RabbitMQ Client thread. -Its architecture is, therefore, actually "`simpler`" than the SMLC. -However, there are some limitations with this approach, and certain features of the SMLC are not available with the DMLC. -Also, concurrency is controlled by `consumersPerQueue` (and the client library's thread pool). -The `concurrentConsumers` and associated properties are not available with this container. - -The following features are available with the SMLC but not the DMLC: - -* `batchSize`: With the SMLC, you can set this to control how many messages are delivered in a transaction or to reduce the number of acks, but it may cause the number of duplicate deliveries to increase after a failure. -(The DMLC does have `messagesPerAck`, which you can use to reduce the acks, the same as with `batchSize` and the SMLC, but it cannot be used with transactions -- each message is delivered and ack'd in a separate transaction). -* `consumerBatchEnabled`: enables batching of discrete messages in the consumer; see <> for more information. -* `maxConcurrentConsumers` and consumer scaling intervals or triggers -- there is no auto-scaling in the DMLC. -It does, however, let you programmatically change the `consumersPerQueue` property and the consumers are adjusted accordingly. - -However, the DMLC has the following benefits over the SMLC: - -* Adding and removing queues at runtime is more efficient. -With the SMLC, the entire consumer thread is restarted (all consumers canceled and re-created). -With the DMLC, unaffected consumers are not canceled. -* The context switch between the RabbitMQ Client thread and the consumer thread is avoided. -* Threads are shared across consumers rather than having a dedicated thread for each consumer in the SMLC. -However, see the IMPORTANT note about the connection factory configuration in <>. - -See <> for information about which configuration properties apply to each container. - -[[idle-containers]] -===== Detecting Idle Asynchronous Consumers - -While efficient, one problem with asynchronous consumers is detecting when they are idle -- users might want to take -some action if no messages arrive for some period of time. - -Starting with version 1.6, it is now possible to configure the listener container to publish a -`ListenerContainerIdleEvent` when some time passes with no message delivery. -While the container is idle, an event is published every `idleEventInterval` milliseconds. - -To configure this feature, set `idleEventInterval` on the container. -The following example shows how to do so in XML and in Java (for both a `SimpleMessageListenerContainer` and a `SimpleRabbitListenerContainerFactory`): - -==== -[source, xml] ----- - - - ----- - -[source, java] ----- -@Bean -public SimpleMessageListenerContainer(ConnectionFactory connectionFactory) { - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); - ... - container.setIdleEventInterval(60000L); - ... - return container; -} ----- - -[source, java] ----- -@Bean -public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(rabbitConnectionFactory()); - factory.setIdleEventInterval(60000L); - ... - return factory; -} ----- -==== - -In each of these cases, an event is published once per minute while the container is idle. - -====== Event Consumption - -You can capture idle events by implementing `ApplicationListener` -- either a general listener, or one narrowed to only -receive this specific event. -You can also use `@EventListener`, introduced in Spring Framework 4.2. - -The following example combines the `@RabbitListener` and `@EventListener` into a single class. -You need to understand that the application listener gets events for all containers, so you may need to -check the listener ID if you want to take specific action based on which container is idle. -You can also use the `@EventListener` `condition` for this purpose. - -The events have four properties: - -* `source`: The listener container instance -* `id`: The listener ID (or container bean name) -* `idleTime`: The time the container had been idle when the event was published -* `queueNames`: The names of the queue(s) that the container listens to - -The following example shows how to create listeners by using both the `@RabbitListener` and the `@EventListener` annotations: - -==== -[source, Java] ----- -public class Listener { - - @RabbitListener(id="someId", queues="#{queue.name}") - public String listen(String foo) { - return foo.toUpperCase(); - } - - @EventListener(condition = "event.listenerId == 'someId'") - public void onApplicationEvent(ListenerContainerIdleEvent event) { - ... - } - -} ----- -==== - -IMPORTANT: Event listeners see events for all containers. -Consequently, in the preceding example, we narrow the events received based on the listener ID. - -CAUTION: If you wish to use the idle event to stop the lister container, you should not call `container.stop()` on the thread that calls the listener. -Doing so always causes delays and unnecessary log messages. -Instead, you should hand off the event to a different thread that can then stop the container. - -[[micrometer]] -===== Monitoring Listener Performance - -Starting with version 2.2, the listener containers will automatically create and update Micrometer `Timer` s for the listener, if `Micrometer` is detected on the class path, and a `MeterRegistry` is present in the application context. -The timers can be disabled by setting the container property `micrometerEnabled` to `false`. - -Two timers are maintained - one for successful calls to the listener and one for failures. -With a simple `MessageListener`, there is a pair of timers for each configured queue. - -The timers are named `spring.rabbitmq.listener` and have the following tags: - -* `listenerId` : (listener id or container bean name) -* `queue` : (the queue name for a simple listener or list of configured queue names when `consumerBatchEnabled` is `true` - because a batch may contain messages from multiple queues) -* `result` : `success` or `failure` -* `exception` : `none` or `ListenerExecutionFailedException` - -You can add additional tags using the `micrometerTags` container property. - -[[containers-and-broker-named-queues]] -==== Containers and Broker-Named queues - -While it is preferable to use `AnonymousQueue` instances as auto-delete queues, starting with version 2.1, you can use broker named queues with listener containers. -The following example shows how to do so: - -==== -[source, java] ----- -@Bean -public Queue queue() { - return new Queue("", false, true, true); -} - -@Bean -public SimpleMessageListenerContainer container() { - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf()); - container.setQueues(queue()); - container.setMessageListener(m -> { - ... - }); - container.setMissingQueuesFatal(false); - return container; -} ----- -==== - -Notice the empty `String` for the name. -When the `RabbitAdmin` declares queues, it updates the `Queue.actualName` property with the name returned by the broker. -You must use `setQueues()` when you configure the container for this to work, so that the container can access the declared name at runtime. -Just setting the names is insufficient. - -NOTE: You cannot add broker-named queues to the containers while they are running. - -IMPORTANT: When a connection is reset and a new one is established, the new queue gets a new name. -Since there is a race condition between the container restarting and the queue being re-declared, it is important to set the container's `missingQueuesFatal` property to `false`, since the container is likely to initially try to reconnect to the old queue. - -[[message-converters]] -==== Message Converters - -The `AmqpTemplate` also defines several methods for sending and receiving messages that delegate to a `MessageConverter`. -The `MessageConverter` provides a single method for each direction: one for converting *to* a `Message` and another for converting *from* a `Message`. -Notice that, when converting to a `Message`, you can also provide properties in addition to the object. -The `object` parameter typically corresponds to the Message body. -The following listing shows the `MessageConverter` interface definition: - -==== -[source,java] ----- -public interface MessageConverter { - - Message toMessage(Object object, MessageProperties messageProperties) - throws MessageConversionException; - - Object fromMessage(Message message) throws MessageConversionException; - -} ----- -==== - -The relevant `Message`-sending methods on the `AmqpTemplate` are simpler than the methods we discussed previously, because they do not require the `Message` instance. -Instead, the `MessageConverter` is responsible for "`creating`" each `Message` by converting the provided object to the byte array for the `Message` body and then adding any provided `MessageProperties`. -The following listing shows the definitions of the various methods: - -==== -[source,java] ----- -void convertAndSend(Object message) throws AmqpException; - -void convertAndSend(String routingKey, Object message) throws AmqpException; - -void convertAndSend(String exchange, String routingKey, Object message) - throws AmqpException; - -void convertAndSend(Object message, MessagePostProcessor messagePostProcessor) - throws AmqpException; - -void convertAndSend(String routingKey, Object message, - MessagePostProcessor messagePostProcessor) throws AmqpException; - -void convertAndSend(String exchange, String routingKey, Object message, - MessagePostProcessor messagePostProcessor) throws AmqpException; ----- -==== - -On the receiving side, there are only two methods: one that accepts the queue name and one that relies on the template's "`queue`" property having been set. -The following listing shows the definitions of the two methods: - -==== -[source,java] ----- -Object receiveAndConvert() throws AmqpException; - -Object receiveAndConvert(String queueName) throws AmqpException; ----- -==== - -NOTE: The `MessageListenerAdapter` mentioned in <> also uses a `MessageConverter`. - -[[simple-message-converter]] -===== `SimpleMessageConverter` - -The default implementation of the `MessageConverter` strategy is called `SimpleMessageConverter`. -This is the converter that is used by an instance of `RabbitTemplate` if you do not explicitly configure an alternative. -It handles text-based content, serialized Java objects, and byte arrays. - -====== Converting From a `Message` - -If the content type of the input `Message` begins with "text" (for example, -"text/plain"), it also checks for the content-encoding property to determine the charset to be used when converting the `Message` body byte array to a Java `String`. -If no content-encoding property had been set on the input `Message`, it uses the UTF-8 charset by default. -If you need to override that default setting, you can configure an instance of `SimpleMessageConverter`, set its `defaultCharset` property, and inject that into a `RabbitTemplate` instance. - -If the content-type property value of the input `Message` is set to "application/x-java-serialized-object", the `SimpleMessageConverter` tries to deserialize (rehydrate) the byte array into a Java object. -While that might be useful for simple prototyping, we do not recommend relying on Java serialization, since it leads to tight coupling between the producer and the consumer. -Of course, it also rules out usage of non-Java systems on either side. -With AMQP being a wire-level protocol, it would be unfortunate to lose much of that advantage with such restrictions. -In the next two sections, we explore some alternatives for passing rich domain object content without relying on Java serialization. - -For all other content-types, the `SimpleMessageConverter` returns the `Message` body content directly as a byte array. - -See <> for important information. - -====== Converting To a `Message` - -When converting to a `Message` from an arbitrary Java Object, the `SimpleMessageConverter` likewise deals with byte arrays, strings, and serializable instances. -It converts each of these to bytes (in the case of byte arrays, there is nothing to convert), and it ses the content-type property accordingly. -If the `Object` to be converted does not match one of those types, the `Message` body is null. - -[[serializer-message-converter]] -===== `SerializerMessageConverter` - -This converter is similar to the `SimpleMessageConverter` except that it can be configured with other Spring Framework -`Serializer` and `Deserializer` implementations for `application/x-java-serialized-object` conversions. - -See <> for important information. - -[[json-message-converter]] -===== Jackson2JsonMessageConverter - -This section covers using the `Jackson2JsonMessageConverter` to convert to and from a `Message`. -It has the following sections: - -* <> -* <> - -[[Jackson2JsonMessageConverter-to-message]] -====== Converting to a `Message` - -As mentioned in the previous section, relying on Java serialization is generally not recommended. -One rather common alternative that is more flexible and portable across different languages and platforms is JSON -(JavaScript Object Notation). -The converter can be configured on any `RabbitTemplate` instance to override its usage of the `SimpleMessageConverter` -default. -The `Jackson2JsonMessageConverter` uses the `com.fasterxml.jackson` 2.x library. -The following example configures a `Jackson2JsonMessageConverter`: - -==== -[source,xml] ----- - - - - - - - - - ----- -==== - -As shown above, `Jackson2JsonMessageConverter` uses a `DefaultClassMapper` by default. -Type information is added to (and retrieved from) `MessageProperties`. -If an inbound message does not contain type information in `MessageProperties`, but you know the expected type, you -can configure a static type by using the `defaultType` property, as the following example shows: - -==== -[source,xml] ----- - - - - - - - ----- -==== - -In addition, you can provide custom mappings from the value in the `__TypeId__` header. -The following example shows how to do so: - -==== -[source, java] ----- -@Bean -public Jackson2JsonMessageConverter jsonMessageConverter() { - Jackson2JsonMessageConverter jsonConverter = new Jackson2JsonMessageConverter(); - jsonConverter.setClassMapper(classMapper()); - return jsonConverter; -} - -@Bean -public DefaultClassMapper classMapper() { - DefaultClassMapper classMapper = new DefaultClassMapper(); - Map> idClassMapping = new HashMap<>(); - idClassMapping.put("thing1", Thing1.class); - idClassMapping.put("thing2", Thing2.class); - classMapper.setIdClassMapping(idClassMapping); - return classMapper; -} ----- -==== - -Now, if the sending system sets the header to `thing1`, the converter creates a `Thing1` object, and so on. -See the <> sample application for a complete discussion about converting messages from non-Spring applications. - -[[Jackson2JsonMessageConverter-from-message]] -====== Converting from a `Message` - -Inbound messages are converted to objects according to the type information added to headers by the sending system. - -In versions prior to 1.6, if type information is not present, conversion would fail. -Starting with version 1.6, if type information is missing, the converter converts the JSON by using Jackson defaults (usually a map). - -Also, starting with version 1.6, when you use `@RabbitListener` annotations (on methods), the inferred type information is added to the `MessageProperties`. -This lets the converter convert to the argument type of the target method. -This only applies if there is one parameter with no annotations or a single parameter with the `@Payload` annotation. -Parameters of type `Message` are ignored during the analysis. - -IMPORTANT: By default, the inferred type information will override the inbound `__TypeId__` and related headers created -by the sending system. -This lets the receiving system automatically convert to a different domain object. -This applies only if the parameter type is concrete (not abstract or an interface) or it is from the `java.util` -package. -In all other cases, the `__TypeId__` and related headers is used. -There are cases where you might wish to override the default behavior and always use the `__TypeId__` information. -For example, suppose you have a `@RabbitListener` that takes a `Thing1` argument but the message contains a `Thing2` that -is a subclass of `Thing1` (which is concrete). -The inferred type would be incorrect. -To handle this situation, set the `TypePrecedence` property on the `Jackson2JsonMessageConverter` to `TYPE_ID` instead -of the default `INFERRED`. -(The property is actually on the converter's `DefaultJackson2JavaTypeMapper`, but a setter is provided on the converter -for convenience.) -If you inject a custom type mapper, you should set the property on the mapper instead. - -NOTE: When converting from the `Message`, an incoming `MessageProperties.getContentType()` must be JSON-compliant (`contentType.contains("json")` is used to check). -Starting with version 2.2, `application/json` is assumed if there is no `contentType` property, or it has the default value `application/octet-stream`. -To revert to the previous behavior (return an unconverted `byte[]`), set the converter's `assumeSupportedContentType` property to `false`. -If the content type is not supported, a `WARN` log message `Could not convert incoming message with content-type [...]`, is emitted and `message.getBody()` is returned as is -- as a `byte[]`. -So, to meet the `Jackson2JsonMessageConverter` requirements on the consumer side, the producer must add the `contentType` message property -- for example, as `application/json` or `text/x-json` or by using the `Jackson2JsonMessageConverter`, which sets the header automatically. -The following listing shows a number of converter calls: - -==== -[source, java] ----- -@RabbitListener -public void thing1(Thing1 thing1) {...} - -@RabbitListener -public void thing1(@Payload Thing1 thing1, @Header("amqp_consumerQueue") String queue) {...} - -@RabbitListener -public void thing1(Thing1 thing1, o.s.amqp.core.Message message) {...} - -@RabbitListener -public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} - -@RabbitListener -public void thing1(Thing1 thing1, String bar) {...} - -@RabbitListener -public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} ----- -==== - -In the first four cases in the preceding listing, the converter tries to convert to the `Thing1` type. -The fifth example is invalid because we cannot determine which argument should receive the message payload. -With the sixth example, the Jackson defaults apply due to the generic type being a `WildcardType`. - -You can, however, create a custom converter and use the `targetMethod` message property to decide which type to convert -the JSON to. - -NOTE: This type inference can only be achieved when the `@RabbitListener` annotation is declared at the method level. -With class-level `@RabbitListener`, the converted type is used to select which `@RabbitHandler` method to invoke. -For this reason, the infrastructure provides the `targetObject` message property, which you can use in a custom -converter to determine the type. - -IMPORTANT: Starting with version 1.6.11, `Jackson2JsonMessageConverter` and, therefore, `DefaultJackson2JavaTypeMapper` (`DefaultClassMapper`) provide the `trustedPackages` option to overcome https://pivotal.io/security/cve-2017-4995[Serialization Gadgets] vulnerability. -By default and for backward compatibility, the `Jackson2JsonMessageConverter` trusts all packages -- that is, it uses `*` for the option. - -[[jackson-abstract]] -====== Deserializing Abstract Classes - -Prior to version 2.2.8, if the inferred type of a `@RabbitListener` was an abstract class (including interfaces), the converter would fall back to looking for type information in the headers and, if present, used that information; if that was not present, it would try to create the abstract class. -This caused a problem when a custom `ObjectMapper` that is configured with a custom deserializer to handle the abstract class is used, but the incoming message has invalid type headers. - -Starting with version 2.2.8, the previous behavior is retained by default. If you have such a custom `ObjectMapper` and you want to ignore type headers, and always use the inferred type for conversion, set the `alwaysConvertToInferredType` to `true`. -This is needed for backwards compatibility and to avoid the overhead of an attempted conversion when it would fail (with a standard `ObjectMapper`). - -[[data-projection]] -====== Using Spring Data Projection Interfaces - -Starting with version 2.2, you can convert JSON to a Spring Data Projection interface instead of a concrete type. -This allows very selective, and low-coupled bindings to data, including the lookup of values from multiple places inside the JSON document. -For example the following interface can be defined as message payload type: - -==== -[source, java] ----- -interface SomeSample { - - @JsonPath({ "$.username", "$.user.name" }) - String getUsername(); - -} ----- -==== - -==== -[source, java] ----- -@RabbitListener(queues = "projection") -public void projection(SomeSample in) { - String username = in.getUsername(); - ... -} ----- -==== - -Accessor methods will be used to lookup the property name as field in the received JSON document by default. -The `@JsonPath` expression allows customization of the value lookup, and even to define multiple JSON path expressions, to lookup values from multiple places until an expression returns an actual value. - -To enable this feature, set the `useProjectionForInterfaces` to `true` on the message converter. -You must also add `spring-data:spring-data-commons` and `com.jayway.jsonpath:json-path` to the class path. - -When used as the parameter to a `@RabbitListener` method, the interface type is automatically passed to the converter as normal. - -[[json-complex]] -====== Converting From a `Message` With `RabbitTemplate` - -As mentioned earlier, type information is conveyed in message headers to assist the converter when converting from a message. -This works fine in most cases. -However, when using generic types, it can only convert simple objects and known "`container`" objects (lists, arrays, and maps). -Starting with version 2.0, the `Jackson2JsonMessageConverter` implements `SmartMessageConverter`, which lets it be used with the new `RabbitTemplate` methods that take a `ParameterizedTypeReference` argument. -This allows conversion of complex generic types, as shown in the following example: - -==== -[source, java] ----- -Thing1> thing1 = - rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference>>() { }); ----- -==== - -NOTE: Starting with version 2.1, the `AbstractJsonMessageConverter` class has been removed. -It is no longer the base class for `Jackson2JsonMessageConverter`. -It has been replaced by `AbstractJackson2MessageConverter`. - -===== `MarshallingMessageConverter` - -Yet another option is the `MarshallingMessageConverter`. -It delegates to the Spring OXM library's implementations of the `Marshaller` and `Unmarshaller` strategy interfaces. -You can read more about that library https://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html[here]. -In terms of configuration, it is most common to provide only the constructor argument, since most implementations of `Marshaller` also implement `Unmarshaller`. -The following example shows how to configure a `MarshallingMessageConverter`: - -==== -[source,xml] ----- - - - - - - - - ----- -==== - -[[jackson2xml]] -===== `Jackson2XmlMessageConverter` - -This class was introduced in version 2.1 and can be used to convert messages from and to XML. - -Both `Jackson2XmlMessageConverter` and `Jackson2JsonMessageConverter` have the same base class: `AbstractJackson2MessageConverter`. - -NOTE: The `AbstractJackson2MessageConverter` class is introduced to replace a removed class: `AbstractJsonMessageConverter`. - -The `Jackson2XmlMessageConverter` uses the `com.fasterxml.jackson` 2.x library. - -You can use it the same way as `Jackson2JsonMessageConverter`, except it supports XML instead of JSON. -The following example configures a `Jackson2JsonMessageConverter`: - -[source,xml] ----- - - - - - - - ----- -See <> for more information. - -NOTE: Starting with version 2.2, `application/xml` is assumed if there is no `contentType` property, or it has the default value `application/octet-stream`. -To revert to the previous behavior (return an unconverted `byte[]`), set the converter's `assumeSupportedContentType` property to `false`. - -===== `ContentTypeDelegatingMessageConverter` - -This class was introduced in version 1.4.2 and allows delegation to a specific `MessageConverter` based on the content type property in the `MessageProperties`. -By default, it delegates to a `SimpleMessageConverter` if there is no `contentType` property or there is a value that matches none of the configured converters. -The following example configures a `ContentTypeDelegatingMessageConverter`: - -==== -[source,xml] ----- - - - - - - - - ----- -==== - -[[java-deserialization]] -===== Java Deserialization - -This section covers how to deserialize Java objects. - -[IMPORTANT] -==== -There is a possible vulnerability when deserializing java objects from untrusted sources. - -If you accept messages from untrusted sources with a `content-type` of `application/x-java-serialized-object`, you should -consider configuring which packages and classes are allowed to be deserialized. -This applies to both the `SimpleMessageConverter` and `SerializerMessageConverter` when it is configured to use a -`DefaultDeserializer` either implicitly or via configuration. - -By default, the allowed list is empty, meaning all classes are deserialized. - -You can set a list of patterns, such as `thing1.*`, `thing1.thing2.Cat` or `*.MySafeClass`. - -The patterns are checked in order until a match is found. -If there is no match, a `SecurityException` is thrown. - -You can set the patterns using the `allowedListPatterns` property on these converters. -==== - -[[message-properties-converters]] -===== Message Properties Converters - -The `MessagePropertiesConverter` strategy interface is used to convert between the Rabbit Client `BasicProperties` and Spring AMQP `MessageProperties`. -The default implementation (`DefaultMessagePropertiesConverter`) is usually sufficient for most purposes, but you can implement your own if needed. -The default properties converter converts `BasicProperties` elements of type `LongString` to `String` instances when the size is not greater than `1024` bytes. -Larger `LongString` instances are not converted (see the next paragraph). -This limit can be overridden with a constructor argument. - -Starting with version 1.6, headers longer than the long string limit (default: 1024) are now left as -`LongString` instances by default by the `DefaultMessagePropertiesConverter`. -You can access the contents through the `getBytes[]`, `toString()`, or `getStream()` methods. - -Previously, the `DefaultMessagePropertiesConverter` "`converted`" such headers to a `DataInputStream` (actually it just referenced the `LongString` instance's `DataInputStream`). -On output, this header was not converted (except to a String -- for example, `java.io.DataInputStream@1d057a39` by calling `toString()` on the stream). - -Large incoming `LongString` headers are now correctly "`converted`" on output, too (by default). - -A new constructor is provided to let you configure the converter to work as before. -The following listing shows the Javadoc comment and declaration of the method: - -==== -[source, java] ----- -/** - * Construct an instance where LongStrings will be returned - * unconverted or as a java.io.DataInputStream when longer than this limit. - * Use this constructor with 'true' to restore pre-1.6 behavior. - * @param longStringLimit the limit. - * @param convertLongLongStrings LongString when false, - * DataInputStream when true. - * @since 1.6 - */ -public DefaultMessagePropertiesConverter(int longStringLimit, boolean convertLongLongStrings) { ... } ----- -==== - -Also starting with version 1.6, a new property called `correlationIdString` has been added to `MessageProperties`. -Previously, when converting to and from `BasicProperties` used by the RabbitMQ client, an unnecessary `byte[] <-> String` conversion was performed because `MessageProperties.correlationId` is a `byte[]`, but `BasicProperties` uses a `String`. -(Ultimately, the RabbitMQ client uses UTF-8 to convert the `String` to bytes to put in the protocol message). - -To provide maximum backwards compatibility, a new property called `correlationIdPolicy` has been added to the -`DefaultMessagePropertiesConverter`. -This takes a `DefaultMessagePropertiesConverter.CorrelationIdPolicy` enum argument. -By default it is set to `BYTES`, which replicates the previous behavior. - -For inbound messages: - -* `STRING`: Only the `correlationIdString` property is mapped -* `BYTES`: Only the `correlationId` property is mapped -* `BOTH`: Both properties are mapped - -For outbound messages: - -* `STRING`: Only the `correlationIdString` property is mapped -* `BYTES`: Only the `correlationId` property is mapped -* `BOTH`: Both properties are considered, with the `String` property taking precedence - -Also starting with version 1.6, the inbound `deliveryMode` property is no longer mapped to `MessageProperties.deliveryMode`. -It is mapped to `MessageProperties.receivedDeliveryMode` instead. -Also, the inbound `userId` property is no longer mapped to `MessageProperties.userId`. -It is mapped to `MessageProperties.receivedUserId` instead. -These changes are to avoid unexpected propagation of these properties if the same `MessageProperties` object is used for an outbound message. - -Starting with version 2.2, the `DefaultMessagePropertiesConverter` converts any custom headers with values of type `Class` using `getName()` instead of `toString()`; this avoids consuming application having to parse the class name out of the `toString()` representation. -For rolling upgrades, you may need to change your consumers to understand both formats until all producers are upgraded. - -[[post-processing]] -==== Modifying Messages - Compression and More - -A number of extension points exist. -They let you perform some processing on a message, either before it is sent to RabbitMQ or immediately after it is received. - -As can be seen in <>, one such extension point is in the `AmqpTemplate` `convertAndReceive` operations, where you can provide a `MessagePostProcessor`. -For example, after your POJO has been converted, the `MessagePostProcessor` lets you set custom headers or properties on the `Message`. - -Starting with version 1.4.2, additional extension points have been added to the `RabbitTemplate` - `setBeforePublishPostProcessors()` and `setAfterReceivePostProcessors()`. -The first enables a post processor to run immediately before sending to RabbitMQ. -When using batching (see <>), this is invoked after the batch is assembled and before the batch is sent. -The second is invoked immediately after a message is received. - -These extension points are used for such features as compression and, for this purpose, several `MessagePostProcessor` implementations are provided. -`GZipPostProcessor`, `ZipPostProcessor` and `DeflaterPostProcessor` compress messages before sending, and `GUnzipPostProcessor`, `UnzipPostProcessor` and `InflaterPostProcessor` decompress received messages. - -NOTE: Starting with version 2.1.5, the `GZipPostProcessor` can be configured with the `copyProperties = true` option to make a copy of the original message properties. -By default, these properties are reused for performance reasons, and modified with compression content encoding and the optional `MessageProperties.SPRING_AUTO_DECOMPRESS` header. -If you retain a reference to the original outbound message, its properties will change as well. -So, if your application retains a copy of an outbound message with these message post processors, consider turning the `copyProperties` option on. - -IMPORTANT: Starting with version 2.2.12, you can configure the delimiter that the compressing post processors use between content encoding elements. -With versions 2.2.11 and before, this was hard-coded as `:`, it is now set to `, ` by default. -The decompressors will work with both delimiters. -However, if you publish messages with 2.3 or later and consume with 2.2.11 or earlier, you MUST set the `encodingDelimiter` property on the compressor(s) to `:`. -When your consumers are upgraded to 2.2.11 or later, you can revert to the default of `, `. - -Similarly, the `SimpleMessageListenerContainer` also has a `setAfterReceivePostProcessors()` method, letting the decompression be performed after messages are received by the container. - -Starting with version 2.1.4, `addBeforePublishPostProcessors()` and `addAfterReceivePostProcessors()` have been added to the `RabbitTemplate` to allow appending new post processors to the list of before publish and after receive post processors respectively. -Also there are methods provided to remove the post processors. -Similarly, `AbstractMessageListenerContainer` also has `addAfterReceivePostProcessors()` and `removeAfterReceivePostProcessor()` methods added. -See the Javadoc of `RabbitTemplate` and `AbstractMessageListenerContainer` for more detail. - -[[request-reply]] -==== Request/Reply Messaging - -The `AmqpTemplate` also provides a variety of `sendAndReceive` methods that accept the same argument options that were described earlier for the one-way send operations (`exchange`, `routingKey`, and `Message`). -Those methods are quite useful for request-reply scenarios, since they handle the configuration of the necessary `reply-to` property before sending and can listen for the reply message on an exclusive queue that is created internally for that purpose. - -Similar request-reply methods are also available where the `MessageConverter` is applied to both the request and reply. -Those methods are named `convertSendAndReceive`. -See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/AmqpTemplate.html[Javadoc of `AmqpTemplate`] for more detail. - -Starting with version 1.5.0, each of the `sendAndReceive` method variants has an overloaded version that takes `CorrelationData`. -Together with a properly configured connection factory, this enables the receipt of publisher confirms for the send side of the operation. -See <> and the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/core/RabbitOperations.html[Javadoc for `RabbitOperations`] for more information. - -Starting with version 2.0, there are variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. -The template must be configured with a `SmartMessageConverter`. -See <> for more information. - -Starting with version 2.1, you can configure the `RabbitTemplate` with the `noLocalReplyConsumer` option to control a `noLocal` flag for reply consumers. -This is `false` by default. - -[[reply-timeout]] -===== Reply Timeout - -By default, the send and receive methods timeout after five seconds and return null. -You can modify this behavior by setting the `replyTimeout` property. -Starting with version 1.5, if you set the `mandatory` property to `true` (or the `mandatory-expression` evaluates to `true` for a particular message), if the message cannot be delivered to a queue, an `AmqpMessageReturnedException` is thrown. -This exception has `returnedMessage`, `replyCode`, and `replyText` properties, as well as the `exchange` and `routingKey` used for the send. - -NOTE: This feature uses publisher returns. -You can enable it by setting `publisherReturns` to `true` on the `CachingConnectionFactory` (see <>). -Also, you must not have registered your own `ReturnCallback` with the `RabbitTemplate`. - -Starting with version 2.1.2, a `replyTimedOut` method has been added, letting subclasses be informed of the timeout so that they can clean up any retained state. - -Starting with versions 2.0.11 and 2.1.3, when you use the default `DirectReplyToMessageListenerContainer`, you can add an error handler by setting the template's `replyErrorHandler` property. -This error handler is invoked for any failed deliveries, such as late replies and messages received without a correlation header. -The exception passed in is a `ListenerExecutionFailedException`, which has a `failedMessage` property. - -[[direct-reply-to]] -===== RabbitMQ Direct reply-to - -IMPORTANT: Starting with version 3.4.0, the RabbitMQ server supports https://www.rabbitmq.com/direct-reply-to.html[direct reply-to]. -This eliminates the main reason for a fixed reply queue (to avoid the need to create a temporary queue for each request). -Starting with Spring AMQP version 1.4.1 direct reply-to is used by default (if supported by the server) instead of creating temporary reply queues. -When no `replyQueue` is provided (or it is set with a name of `amq.rabbitmq.reply-to`), the `RabbitTemplate` automatically detects whether direct reply-to is supported and either uses it or falls back to using a temporary reply queue. -When using direct reply-to, a `reply-listener` is not required and should not be configured. - -Reply listeners are still supported with named queues (other than `amq.rabbitmq.reply-to`), allowing control of reply concurrency and so on. - -Starting with version 1.6, if you wish to use a temporary, exclusive, auto-delete queue for each -reply, set the `useTemporaryReplyQueues` property to `true`. -This property is ignored if you set a `replyAddress`. - -You can change the criteria that dictate whether to use direct reply-to by subclassing `RabbitTemplate` and overriding `useDirectReplyTo()` to check different criteria. -The method is called once only, when the first request is sent. - -Prior to version 2.0, the `RabbitTemplate` created a new consumer for each request and canceled the consumer when the reply was received (or timed out). -Now the template uses a `DirectReplyToMessageListenerContainer` instead, letting the consumers be reused. -The template still takes care of correlating the replies, so there is no danger of a late reply going to a different sender. -If you want to revert to the previous behavior, set the `useDirectReplyToContainer` (`direct-reply-to-container` when using XML configuration) property to false. - -The `AsyncRabbitTemplate` has no such option. -It always used a `DirectReplyToContainer` for replies when direct reply-to is used. - -Starting with version 2.3.7, the template has a new property `useChannelForCorrelation`. -When this is `true`, the server does not have to copy the correlation id from the request message headers to the reply message. -Instead, the channel used to send the request is used to correlate the reply to the request. - -===== Message Correlation With A Reply Queue - -When using a fixed reply queue (other than `amq.rabbitmq.reply-to`), you must provide correlation data so that replies can be correlated to requests. -See https://www.rabbitmq.com/tutorials/tutorial-six-java.html[RabbitMQ Remote Procedure Call (RPC)]. -By default, the standard `correlationId` property is used to hold the correlation data. -However, if you wish to use a custom property to hold correlation data, you can set the `correlation-key` attribute on the . -Explicitly setting the attribute to `correlationId` is the same as omitting the attribute. -The client and server must use the same header for correlation data. - -NOTE: Spring AMQP version 1.1 used a custom property called `spring_reply_correlation` for this data. -If you wish to revert to this behavior with the current version (perhaps to maintain compatibility with another application using 1.1), you must set the attribute to `spring_reply_correlation`. - -By default, the template generates its own correlation ID (ignoring any user-supplied value). -If you wish to use your own correlation ID, set the `RabbitTemplate` instance's `userCorrelationId` property to `true`. - -IMPORTANT: The correlation ID must be unique to avoid the possibility of a wrong reply being returned for a request. - -[[reply-listener]] -===== Reply Listener Container - -When using RabbitMQ versions prior to 3.4.0, a new temporary queue is used for each reply. -However, a single reply queue can be configured on the template, which can be more efficient and also lets you set arguments on that queue. -In this case, however, you must also provide a sub element. -This element provides a listener container for the reply queue, with the template being the listener. -All of the <> attributes allowed on a are allowed on the element, except for `connection-factory` and `message-converter`, which are inherited from the template's configuration. - -IMPORTANT: If you run multiple instances of your application or use multiple `RabbitTemplate` instances, you *MUST* use a unique reply queue for each. -RabbitMQ has no ability to select messages from a queue, so, if they all use the same queue, each instance would compete for replies and not necessarily receive their own. - -The following example defines a rabbit template with a connection factory: - -==== -[source,xml] ----- - - - ----- -==== - -While the container and template share a connection factory, they do not share a channel. -Therefore, requests and replies are not performed within the same transaction (if transactional). - -NOTE: Prior to version 1.5.0, the `reply-address` attribute was not available. -Replies were always routed by using the default exchange and the `reply-queue` name as the routing key. -This is still the default, but you can now specify the new `reply-address` attribute. -The `reply-address` can contain an address with the form `/` and the reply is routed to the specified exchange and routed to a queue bound with the routing key. -The `reply-address` has precedence over `reply-queue`. -When only `reply-address` is in use, the `` must be configured as a separate `` component. -The `reply-address` and `reply-queue` (or `queues` attribute on the ``) must refer to the same queue logically. - -With this configuration, a `SimpleListenerContainer` is used to receive the replies, with the `RabbitTemplate` being the `MessageListener`. -When defining a template with the `` namespace element, as shown in the preceding example, the parser defines the container and wires in the template as the listener. - -NOTE: When the template does not use a fixed `replyQueue` (or is using direct reply-to -- see <>), a listener container is not needed. -Direct `reply-to` is the preferred mechanism when using RabbitMQ 3.4.0 or later. - -If you define your `RabbitTemplate` as a `` or use an `@Configuration` class to define it as an `@Bean` or when you create the template programmatically, you need to define and wire up the reply listener container yourself. -If you fail to do this, the template never receives the replies and eventually times out and returns null as the reply to a call to a `sendAndReceive` method. - -Starting with version 1.5, the `RabbitTemplate` detects if it has been -configured as a `MessageListener` to receive replies. -If not, attempts to send and receive messages with a reply address -fail with an `IllegalStateException` (because the replies are never received). - -Further, if a simple `replyAddress` (queue name) is used, the reply listener container verifies that it is listening -to a queue with the same name. -This check cannot be performed if the reply address is an exchange and routing key and a debug log message is written. - -IMPORTANT: When wiring the reply listener and template yourself, it is important to ensure that the template's `replyAddress` and the container's `queues` (or `queueNames`) properties refer to the same queue. -The template inserts the reply address into the outbound message `replyTo` property. - -The following listing shows examples of how to manually wire up the beans: - -==== -[source,xml] ----- - - - - - - - - - - - - - - - - ----- - -[source,java] ----- - @Bean - public RabbitTemplate amqpTemplate() { - RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory()); - rabbitTemplate.setMessageConverter(msgConv()); - rabbitTemplate.setReplyAddress(replyQueue().getName()); - rabbitTemplate.setReplyTimeout(60000); - rabbitTemplate.setUseDirectReplyToContainer(false); - return rabbitTemplate; - } - - @Bean - public SimpleMessageListenerContainer replyListenerContainer() { - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); - container.setConnectionFactory(connectionFactory()); - container.setQueues(replyQueue()); - container.setMessageListener(amqpTemplate()); - return container; - } - - @Bean - public Queue replyQueue() { - return new Queue("my.reply.queue"); - } ----- -==== - -A complete example of a `RabbitTemplate` wired with a fixed reply queue, together with a "`remote`" listener container that handles the request and returns the reply is shown in https://github.com/spring-projects/spring-amqp/tree/main/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java[this test case]. - -IMPORTANT: When the reply times out (`replyTimeout`), the `sendAndReceive()` methods return null. - -Prior to version 1.3.6, late replies for timed out messages were only logged. -Now, if a late reply is received, it is rejected (the template throws an `AmqpRejectAndDontRequeueException`). -If the reply queue is configured to send rejected messages to a dead letter exchange, the reply can be retrieved for later analysis. -To do so, bind a queue to the configured dead letter exchange with a routing key equal to the reply queue's name. - -See the https://www.rabbitmq.com/dlx.html[RabbitMQ Dead Letter Documentation] for more information about configuring dead lettering. -You can also take a look at the `FixedReplyQueueDeadLetterTests` test case for an example. - -[[async-template]] -===== Async Rabbit Template - -Version 1.6 introduced the `AsyncRabbitTemplate`. -This has similar `sendAndReceive` (and `convertSendAndReceive`) methods to those on the <>. -However, instead of blocking, they return a `ListenableFuture`. - -The `sendAndReceive` methods return a `RabbitMessageFuture`. -The `convertSendAndReceive` methods return a `RabbitConverterFuture`. - -You can either synchronously retrieve the result later, by invoking `get()` on the future, or you can register a callback that is called asynchronously with the result. -The following listing shows both approaches: - -==== -[source, java] ----- -@Autowired -private AsyncRabbitTemplate template; - -... - -public void doSomeWorkAndGetResultLater() { - - ... - - ListenableFuture future = this.template.convertSendAndReceive("foo"); - - // do some more work - - String reply = null; - try { - reply = future.get(); - } - catch (ExecutionException e) { - ... - } - - ... - -} - -public void doSomeWorkAndGetResultAsync() { - - ... - - RabbitConverterFuture future = this.template.convertSendAndReceive("foo"); - future.addCallback(new ListenableFutureCallback() { - - @Override - public void onSuccess(String result) { - ... - } - - @Override - public void onFailure(Throwable ex) { - ... - } - - }); - - ... - -} ----- -==== - -If `mandatory` is set and the message cannot be delivered, the future throws an `ExecutionException` with a cause of `AmqpMessageReturnedException`, which encapsulates the returned message and information about the return. - -If `enableConfirms` is set, the future has a property called `confirm`, which is itself a `ListenableFuture` with `true` indicating a successful publish. -If the confirm future is `false`, the `RabbitFuture` has a further property called `nackCause`, which contains the reason for the failure, if available. - -IMPORTANT: The publisher confirm is discarded if it is received after the reply, since the reply implies a successful publish. - -You can set the `receiveTimeout` property on the template to time out replies (it defaults to `30000` - 30 seconds). -If a timeout occurs, the future is completed with an `AmqpReplyTimeoutException`. - -The template implements `SmartLifecycle`. -Stopping the template while there are pending replies causes the pending `Future` instances to be canceled. - -Starting with version 2.0, the asynchronous template now supports https://www.rabbitmq.com/direct-reply-to.html[direct reply-to] instead of a configured reply queue. -To enable this feature, use one of the following constructors: - -==== -[source, java] ----- -public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey) - -public AsyncRabbitTemplate(RabbitTemplate template) ----- -==== - -See <> to use direct reply-to with the synchronous `RabbitTemplate`. - -Version 2.0 introduced variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. -You must configure the underlying `RabbitTemplate` with a `SmartMessageConverter`. -See <> for more information. - -[[remoting]] -===== Spring Remoting with AMQP - -The Spring Framework has a general remoting capability, allowing https://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html[Remote Procedure Calls (RPC)] that use various transports. -Spring-AMQP supports a similar mechanism with a `AmqpProxyFactoryBean` on the client and a `AmqpInvokerServiceExporter` on the server. -This provides RPC over AMQP. -On the client side, a `RabbitTemplate` is used as described <>. -On the server side, the invoker (configured as a `MessageListener`) receives the message, invokes the configured service, and returns the reply by using the inbound message's `replyTo` information. - -You can inject the client factory bean into any bean (by using its `serviceInterface`). -The client can then invoke methods on the proxy, resulting in remote execution over AMQP. - -NOTE: With the default `MessageConverter` instances, the method parameters and returned value must be instances of `Serializable`. - -On the server side, the `AmqpInvokerServiceExporter` has both `AmqpTemplate` and `MessageConverter` properties. -Currently, the template's `MessageConverter` is not used. -If you need to supply a custom message converter, you should provide it by setting the `messageConverter` property. -On the client side, you can add a custom message converter to the `AmqpTemplate`, which is provided to the `AmqpProxyFactoryBean` by using its `amqpTemplate` property. - -The following listing shows sample client and server configurations: - -==== -[source,xml] ----- - - - - - - - - - - - - - - - - - - ----- - -[source,xml] ----- - - - - - - - - - - - - - - - - - ----- -==== - -IMPORTANT: The `AmqpInvokerServiceExporter` can process only properly formed messages, such as those sent from the `AmqpProxyFactoryBean`. -If it receives a message that it cannot interpret, a serialized `RuntimeException` is sent as a reply. -If the message has no `replyToAddress` property, the message is rejected and permanently lost if no dead letter exchange has been configured. - -NOTE: By default, if the request message cannot be delivered, the calling thread eventually times out and a `RemoteProxyFailureException` is thrown. -By default, the timeout is five seconds. -You can modify that duration by setting the `replyTimeout` property on the `RabbitTemplate`. -Starting with version 1.5, by setting the `mandatory` property to `true` and enabling returns on the connection factory (see <>), the calling thread throws an `AmqpMessageReturnedException`. -See <> for more information. - -[[broker-configuration]] -==== Configuring the Broker - -The AMQP specification describes how the protocol can be used to configure queues, exchanges, and bindings on the broker. -These operations (which are portable from the 0.8 specification and higher) are present in the `AmqpAdmin` interface in the `org.springframework.amqp.core` package. -The RabbitMQ implementation of that class is `RabbitAdmin` located in the `org.springframework.amqp.rabbit.core` package. - -The `AmqpAdmin` interface is based on using the Spring AMQP domain abstractions and is shown in the following listing: - -==== -[source,java] ----- -public interface AmqpAdmin { - - // Exchange Operations - - void declareExchange(Exchange exchange); - - void deleteExchange(String exchangeName); - - // Queue Operations - - Queue declareQueue(); - - String declareQueue(Queue queue); - - void deleteQueue(String queueName); - - void deleteQueue(String queueName, boolean unused, boolean empty); - - void purgeQueue(String queueName, boolean noWait); - - // Binding Operations - - void declareBinding(Binding binding); - - void removeBinding(Binding binding); - - Properties getQueueProperties(String queueName); - -} ----- -==== - -See also <>. - -The `getQueueProperties()` method returns some limited information about the queue (message count and consumer count). -The keys for the properties returned are available as constants in the `RabbitTemplate` (`QUEUE_NAME`, -`QUEUE_MESSAGE_COUNT`, and `QUEUE_CONSUMER_COUNT`). -The <> provides much more information in the `QueueInfo` object. - -The no-arg `declareQueue()` method defines a queue on the broker with a name that is automatically generated. -The additional properties of this auto-generated queue are `exclusive=true`, `autoDelete=true`, and `durable=false`. - -The `declareQueue(Queue queue)` method takes a `Queue` object and returns the name of the declared queue. -If the `name` property of the provided `Queue` is an empty `String`, the broker declares the queue with a generated name. -That name is returned to the caller. -That name is also added to the `actualName` property of the `Queue`. -You can use this functionality programmatically only by invoking the `RabbitAdmin` directly. -When using auto-declaration by the admin when defining a queue declaratively in the application context, you can set the name property to `""` (the empty string). -The broker then creates the name. -Starting with version 2.1, listener containers can use queues of this type. -See <> for more information. - -This is in contrast to an `AnonymousQueue` where the framework generates a unique (`UUID`) name and sets `durable` to -`false` and `exclusive`, `autoDelete` to `true`. -A `` with an empty (or missing) `name` attribute always creates an `AnonymousQueue`. - -See <> to understand why `AnonymousQueue` is preferred over broker-generated queue names as well as -how to control the format of the name. -Starting with version 2.1, anonymous queues are declared with argument `Queue.X_QUEUE_LEADER_LOCATOR` set to `client-local` by default. -This ensures that the queue is declared on the node to which the application is connected. -Declarative queues must have fixed names because they might be referenced elsewhere in the context -- such as in the -listener shown in the following example: - -==== -[source,xml] ----- - - - ----- -==== - -See <>. - -The RabbitMQ implementation of this interface is `RabbitAdmin`, which, when configured by using Spring XML, resembles the following example: - -==== -[source,xml] ----- - - - ----- -==== - -When the `CachingConnectionFactory` cache mode is `CHANNEL` (the default), the `RabbitAdmin` implementation does automatic lazy declaration of queues, exchanges, and bindings declared in the same `ApplicationContext`. -These components are declared as soon as a `Connection` is opened to the broker. -There are some namespace features that make this very convenient -- for example, -in the Stocks sample application, we have the following: - -==== -[source,xml] ----- - - - - - - - - - - - - - - - ----- -==== - -In the preceding example, we use anonymous queues (actually, internally, just queues with names generated by the framework, not by the broker) and refer to them by ID. -We can also declare queues with explicit names, which also serve as identifiers for their bean definitions in the context. -The following example configures a queue with an explicit name: - -==== -[source,xml] ----- - ----- -==== - -TIP: You can provide both `id` and `name` attributes. -This lets you refer to the queue (for example, in a binding) by an ID that is independent of the queue name. -It also allows standard Spring features (such as property placeholders and SpEL expressions for the queue name). -These features are not available when you use the name as the bean identifier. - -Queues can be configured with additional arguments -- for example, `x-message-ttl`. -When you use the namespace support, they are provided in the form of a `Map` of argument-name/argument-value pairs, which are defined by using the `` element. -The following example shows how to do so: - -==== -[source,xml] ----- - - - - - - ----- -==== - -By default, the arguments are assumed to be strings. -For arguments of other types, you must provide the type. -The following example shows how to specify the type: - -==== -[source,xml] ----- - - - - - ----- -==== - -When providing arguments of mixed types, you must provide the type for each entry element. -The following example shows how to do so: - -==== -[source,xml] ----- - - - - 100 - - - - - ----- -==== - -With Spring Framework 3.2 and later, this can be declared a little more succinctly, as follows: - -==== -[source,xml] ----- - - - - - - ----- -==== - -When you use Java configuration, the `Queue.X_QUEUE_LEADER_LOCATOR` argument is supported as a first class property through the `setLeaderLocator()` method on the `Queue` class. -Starting with version 2.1, anonymous queues are declared with this property set to `client-local` by default. -This ensures that the queue is declared on the node the application is connected to. - -IMPORTANT: The RabbitMQ broker does not allow declaration of a queue with mismatched arguments. -For example, if a `queue` already exists with no `time to live` argument, and you attempt to declare it with (for example) `key="x-message-ttl" value="100"`, an exception is thrown. - -By default, the `RabbitAdmin` immediately stops processing all declarations when any exception occurs. -This could cause downstream issues, such as a listener container failing to initialize because another queue (defined after the one in error) is not declared. - -This behavior can be modified by setting the `ignore-declaration-exceptions` attribute to `true` on the `RabbitAdmin` instance. -This option instructs the `RabbitAdmin` to log the exception and continue declaring other elements. -When configuring the `RabbitAdmin` using Java, this property is called `ignoreDeclarationExceptions`. -This is a global setting that applies to all elements. -Queues, exchanges, and bindings have a similar property that applies to just those elements. - -Prior to version 1.6, this property took effect only if an `IOException` occurred on the channel, such as when there is a mismatch between current and desired properties. -Now, this property takes effect on any exception, including `TimeoutException` and others. - -In addition, any declaration exceptions result in the publishing of a `DeclarationExceptionEvent`, which is an `ApplicationEvent` that can be consumed by any `ApplicationListener` in the context. -The event contains a reference to the admin, the element that was being declared, and the `Throwable`. - -[[headers-exchange]] -===== Headers Exchange - -Starting with version 1.3, you can configure the `HeadersExchange` to match on multiple headers. -You can also specify whether any or all headers must match. -The following example shows how to do so: - -==== -[source,xml] ----- - - - - - - - - - - - ----- -==== - -Starting with version 1.6, you can configure `Exchanges` with an `internal` flag (defaults to `false`) and such an -`Exchange` is properly configured on the Broker through a `RabbitAdmin` (if one is present in the application context). -If the `internal` flag is `true` for an exchange, RabbitMQ does not let clients use the exchange. -This is useful for a dead letter exchange or exchange-to-exchange binding, where you do not wish the exchange to be used -directly by publishers. - -To see how to use Java to configure the AMQP infrastructure, look at the Stock sample application, -where there is the `@Configuration` class `AbstractStockRabbitConfiguration`, which ,in turn has -`RabbitClientConfiguration` and `RabbitServerConfiguration` subclasses. -The following listing shows the code for `AbstractStockRabbitConfiguration`: - -==== -[source,java] ----- -@Configuration -public abstract class AbstractStockAppRabbitConfiguration { - - @Bean - public CachingConnectionFactory connectionFactory() { - CachingConnectionFactory connectionFactory = - new CachingConnectionFactory("localhost"); - connectionFactory.setUsername("guest"); - connectionFactory.setPassword("guest"); - return connectionFactory; - } - - @Bean - public RabbitTemplate rabbitTemplate() { - RabbitTemplate template = new RabbitTemplate(connectionFactory()); - template.setMessageConverter(jsonMessageConverter()); - configureRabbitTemplate(template); - return template; - } - - @Bean - public Jackson2JsonMessageConverter jsonMessageConverter() { - return new Jackson2JsonMessageConverter(); - } - - @Bean - public TopicExchange marketDataExchange() { - return new TopicExchange("app.stock.marketdata"); - } - - // additional code omitted for brevity - -} ----- -==== - -In the Stock application, the server is configured by using the following `@Configuration` class: - -==== -[source,java] ----- -@Configuration -public class RabbitServerConfiguration extends AbstractStockAppRabbitConfiguration { - - @Bean - public Queue stockRequestQueue() { - return new Queue("app.stock.request"); - } -} ----- -==== - -This is the end of the whole inheritance chain of `@Configuration` classes. -The end result is that `TopicExchange` and `Queue` are declared to the broker upon application startup. -There is no binding of `TopicExchange` to a queue in the server configuration, as that is done in the client application. -The stock request queue, however, is automatically bound to the AMQP default exchange. -This behavior is defined by the specification. - -The client `@Configuration` class is a little more interesting. -Its declaration follows: - -==== -[source,java] ----- -@Configuration -public class RabbitClientConfiguration extends AbstractStockAppRabbitConfiguration { - - @Value("${stocks.quote.pattern}") - private String marketDataRoutingKey; - - @Bean - public Queue marketDataQueue() { - return amqpAdmin().declareQueue(); - } - - /** - * Binds to the market data exchange. - * Interested in any stock quotes - * that match its routing key. - */ - @Bean - public Binding marketDataBinding() { - return BindingBuilder.bind( - marketDataQueue()).to(marketDataExchange()).with(marketDataRoutingKey); - } - - // additional code omitted for brevity - -} ----- -==== - -The client declares another queue through the `declareQueue()` method on the `AmqpAdmin`. -It binds that queue to the market data exchange with a routing pattern that is externalized in a properties file. - - -[[builder-api]] -===== Builder API for Queues and Exchanges - -Version 1.6 introduces a convenient fluent API for configuring `Queue` and `Exchange` objects when using Java configuration. -The following example shows how to use it: - -==== -[source, java] ----- -@Bean -public Queue queue() { - return QueueBuilder.nonDurable("foo") - .autoDelete() - .exclusive() - .withArgument("foo", "bar") - .build(); -} - -@Bean -public Exchange exchange() { - return ExchangeBuilder.directExchange("foo") - .autoDelete() - .internal() - .withArgument("foo", "bar") - .build(); -} ----- -==== - -See the Javadoc for https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. - -Starting with version 2.0, the `ExchangeBuilder` now creates durable exchanges by default, to be consistent with the simple constructors on the individual `AbstractExchange` classes. -To make a non-durable exchange with the builder, use `.durable(false)` before invoking `.build()`. -The `durable()` method with no parameter is no longer provided. - -Version 2.2 introduced fluent APIs to add "well known" exchange and queue arguments... - -==== -[source, java] ----- -@Bean -public Queue allArgs1() { - return QueueBuilder.nonDurable("all.args.1") - .ttl(1000) - .expires(200_000) - .maxLength(42) - .maxLengthBytes(10_000) - .overflow(Overflow.rejectPublish) - .deadLetterExchange("dlx") - .deadLetterRoutingKey("dlrk") - .maxPriority(4) - .lazy() - .leaderLocator(LeaderLocator.minLeaders) - .singleActiveConsumer() - .build(); -} - -@Bean -public DirectExchange ex() { - return ExchangeBuilder.directExchange("ex.with.alternate") - .durable(true) - .alternate("alternate") - .build(); -} ----- -==== - -[[collection-declaration]] -===== Declaring Collections of Exchanges, Queues, and Bindings - -You can wrap collections of `Declarable` objects (`Queue`, `Exchange`, and `Binding`) in `Declarables` objects. -The `RabbitAdmin` detects such beans (as well as discrete `Declarable` beans) in the application context, and declares the contained objects on the broker whenever a connection is established (initially and after a connection failure). -The following example shows how to do so: - -==== -[source, java] ----- -@Configuration -public static class Config { - - @Bean - public CachingConnectionFactory cf() { - return new CachingConnectionFactory("localhost"); - } - - @Bean - public RabbitAdmin admin(ConnectionFactory cf) { - return new RabbitAdmin(cf); - } - - @Bean - public DirectExchange e1() { - return new DirectExchange("e1", false, true); - } - - @Bean - public Queue q1() { - return new Queue("q1", false, false, true); - } - - @Bean - public Binding b1() { - return BindingBuilder.bind(q1()).to(e1()).with("k1"); - } - - @Bean - public Declarables es() { - return new Declarables( - new DirectExchange("e2", false, true), - new DirectExchange("e3", false, true)); - } - - @Bean - public Declarables qs() { - return new Declarables( - new Queue("q2", false, false, true), - new Queue("q3", false, false, true)); - } - - @Bean - @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) - public Declarables prototypes() { - return new Declarables(new Queue(this.prototypeQueueName, false, false, true)); - } - - @Bean - public Declarables bs() { - return new Declarables( - new Binding("q2", DestinationType.QUEUE, "e2", "k2", null), - new Binding("q3", DestinationType.QUEUE, "e3", "k3", null)); - } - - @Bean - public Declarables ds() { - return new Declarables( - new DirectExchange("e4", false, true), - new Queue("q4", false, false, true), - new Binding("q4", DestinationType.QUEUE, "e4", "k4", null)); - } - -} ----- -==== - -IMPORTANT: In versions prior to 2.1, you could declare multiple `Declarable` instances by defining beans of type `Collection`. -This can cause undesirable side effects in some cases, because the admin has to iterate over all `Collection` beans. -This feature is now disabled in favor of `Declarables`, as discussed earlier in this section. -You can revert to the previous behavior by setting the `RabbitAdmin` property called `declareCollections` to `true`. - -Version 2.2 added the `getDeclarablesByType` method to `Declarables`; this can be used as a convenience, for example, when declaring the listener container bean(s). - -==== -[source, java] ----- -public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, - Declarables mixedDeclarables, MessageListener listener) { - - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); - container.setQueues(mixedDeclarables.getDeclarablesByType(Queue.class).toArray(new Queue[0])); - container.setMessageListener(listener); - return container; -} ----- -==== - -[[conditional-declaration]] -===== Conditional Declaration - -By default, all queues, exchanges, and bindings are declared by all `RabbitAdmin` instances (assuming they have `auto-startup="true"`) in the application context. - -Starting with version 2.1.9, the `RabbitAdmin` has a new property `explicitDeclarationsOnly` (which is `false` by default); when this is set to `true`, the admin will only declare beans that are explicitly configured to be declared by that admin. - -NOTE: Starting with the 1.2 release, you can conditionally declare these elements. -This is particularly useful when an application connects to multiple brokers and needs to specify with which brokers a particular element should be declared. - -The classes representing these elements implement `Declarable`, which has two methods: `shouldDeclare()` and `getDeclaringAdmins()`. -The `RabbitAdmin` uses these methods to determine whether a particular instance should actually process the declarations on its `Connection`. - -The properties are available as attributes in the namespace, as shown in the following examples: - -==== -[source,xml] ----- - - - - - - - - - - - - - - - - - - - ----- -==== - -NOTE: By default, the `auto-declare` attribute is `true` and, if the `declared-by` is not supplied (or is empty), then all `RabbitAdmin` instances declare the object (as long as the admin's `auto-startup` attribute is `true`, the default, and the admin's `explicit-declarations-only` attribute is false). - -Similarly, you can use Java-based `@Configuration` to achieve the same effect. -In the following example, the components are declared by `admin1` but not by`admin2`: - -==== -[source,java] ----- -@Bean -public RabbitAdmin admin1() { - return new RabbitAdmin(cf1()); -} - -@Bean -public RabbitAdmin admin2() { - return new RabbitAdmin(cf2()); -} - -@Bean -public Queue queue() { - Queue queue = new Queue("foo"); - queue.setAdminsThatShouldDeclare(admin1()); - return queue; -} - -@Bean -public Exchange exchange() { - DirectExchange exchange = new DirectExchange("bar"); - exchange.setAdminsThatShouldDeclare(admin1()); - return exchange; -} - -@Bean -public Binding binding() { - Binding binding = new Binding("foo", DestinationType.QUEUE, exchange().getName(), "foo", null); - binding.setAdminsThatShouldDeclare(admin1()); - return binding; -} ----- -==== - -[[note-id-name]] -===== A Note On the `id` and `name` Attributes - -The `name` attribute on `` and `` elements reflects the name of the entity in the broker. -For queues, if the `name` is omitted, an anonymous queue is created (see <>). - -In versions prior to 2.0, the `name` was also registered as a bean name alias (similar to `name` on `` elements). - -This caused two problems: - -* It prevented the declaration of a queue and exchange with the same name. -* The alias was not resolved if it contained a SpEL expression (`#{...}`). - -Starting with version 2.0, if you declare one of these elements with both an `id` _and_ a `name` attribute, the name is no longer declared as a bean name alias. -If you wish to declare a queue and exchange with the same `name`, you must provide an `id`. - -There is no change if the element has only a `name` attribute. -The bean can still be referenced by the `name` -- for example, in binding declarations. -However, you still cannot reference it if the name contains SpEL -- you must provide an `id` for reference purposes. - - -[[anonymous-queue]] -===== `AnonymousQueue` - -In general, when you need a uniquely-named, exclusive, auto-delete queue, we recommend that you use the `AnonymousQueue` -instead of broker-defined queue names (using `""` as a `Queue` name causes the broker to generate the queue -name). - -This is because: - -. The queues are actually declared when the connection to the broker is established. -This is long after the beans are created and wired together. -Beans that use the queue need to know its name. -In fact, the broker might not even be running when the application is started. -. If the connection to the broker is lost for some reason, the admin re-declares the `AnonymousQueue` with the same name. -If we used broker-declared queues, the queue name would change. - -You can control the format of the queue name used by `AnonymousQueue` instances. - -By default, the queue name is prefixed by `spring.gen-` followed by a base64 representation of the `UUID` -- for example: `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. - -You can provide an `AnonymousQueue.NamingStrategy` implementation in a constructor argument. -The following example shows how to do so: - -==== -[source, java] ----- -@Bean -public Queue anon1() { - return new AnonymousQueue(); -} - -@Bean -public Queue anon2() { - return new AnonymousQueue(new AnonymousQueue.Base64UrlNamingStrategy("something-")); -} - -@Bean -public Queue anon3() { - return new AnonymousQueue(AnonymousQueue.UUIDNamingStrategy.DEFAULT); -} ----- -==== - -The first bean generates a queue name prefixed by `spring.gen-` followed by a base64 representation of the `UUID` -- for -example: `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. -The second bean generates a queue name prefixed by `something-` followed by a base64 representation of the `UUID`. -The third bean generates a name by using only the UUID (no base64 conversion) -- for example, `f20c818a-006b-4416-bf91-643590fedb0e`. - -The base64 encoding uses the "`URL and Filename Safe Alphabet`" from RFC 4648. -Trailing padding characters (`=`) are removed. - -You can provide your own naming strategy, whereby you can include other information (such as the application name or client host) in the queue name. - -You can specify the naming strategy when you use XML configuration. -The `naming-strategy` attribute is present on the `` element -for a bean reference that implements `AnonymousQueue.NamingStrategy`. -The following examples show how to specify the naming strategy in various ways: - -==== -[source, xml] ----- - - - - - - - - - - - ----- -==== - -The first example creates names such as `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. -The second example creates names with a String representation of a UUID. -The third example creates names such as `custom.gen-MRBv9sqISkuCiPfOYfpo4g`. - -You can also provide your own naming strategy bean. - -Starting with version 2.1, anonymous queues are declared with argument `Queue.X_QUEUE_LEADER_LOCATOR` set to `client-local` by default. -This ensures that the queue is declared on the node to which the application is connected. -You can revert to the previous behavior by calling `queue.setLeaderLocator(null)` after constructing the instance. - -[[broker-events]] -==== Broker Event Listener - -When the https://www.rabbitmq.com/event-exchange.html[Event Exchange Plugin] is enabled, if you add a bean of type `BrokerEventListener` to the application context, it publishes selected broker events as `BrokerEvent` instances, which can be consumed with a normal Spring `ApplicationListener` or `@EventListener` method. -Events are published by the broker to a topic exchange `amq.rabbitmq.event` with a different routing key for each event type. -The listener uses event keys, which are used to bind an `AnonymousQueue` to the exchange so the listener receives only selected events. -Since it is a topic exchange, wildcards can be used (as well as explicitly requesting specific events), as the following example shows: - -==== -[source, java] ----- -@Bean -public BrokerEventListener eventListener() { - return new BrokerEventListener(connectionFactory(), "user.deleted", "channel.#", "queue.#"); -} ----- -==== - -You can further narrow the received events in individual event listeners, by using normal Spring techniques, as the following example shows: - -==== -[source, java] ----- -@EventListener(condition = "event.eventType == 'queue.created'") -public void listener(BrokerEvent event) { - ... -} ----- -==== - -[[delayed-message-exchange]] -==== Delayed Message Exchange - -Version 1.6 introduces support for the -https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq/[Delayed Message Exchange Plugin] - -NOTE: The plugin is currently marked as experimental but has been available for over a year (at the time of writing). -If changes to the plugin make it necessary, we plan to add support for such changes as soon as practical. -For that reason, this support in Spring AMQP should be considered experimental, too. -This functionality was tested with RabbitMQ 3.6.0 and version 0.0.1 of the plugin. - -To use a `RabbitAdmin` to declare an exchange as delayed, you can set the `delayed` property on the exchange bean to -`true`. -The `RabbitAdmin` uses the exchange type (`Direct`, `Fanout`, and so on) to set the `x-delayed-type` argument and -declare the exchange with type `x-delayed-message`. - -The `delayed` property (default: `false`) is also available when configuring exchange beans using XML. -The following example shows how to use it: - -==== -[source, xml] ----- - ----- -==== - -To send a delayed message, you can set the `x-delay` header through `MessageProperties`, as the following examples show: - -==== -[source, java] ----- -MessageProperties properties = new MessageProperties(); -properties.setDelay(15000); -template.send(exchange, routingKey, - MessageBuilder.withBody("foo".getBytes()).andProperties(properties).build()); ----- - -[source, java] ----- -rabbitTemplate.convertAndSend(exchange, routingKey, "foo", new MessagePostProcessor() { - - @Override - public Message postProcessMessage(Message message) throws AmqpException { - message.getMessageProperties().setDelay(15000); - return message; - } - -}); ----- -==== - -To check if a message was delayed, use the `getReceivedDelay()` method on the `MessageProperties`. -It is a separate property to avoid unintended propagation to an output message generated from an input message. - - -[[management-rest-api]] -==== RabbitMQ REST API - -When the management plugin is enabled, the RabbitMQ server exposes a REST API to monitor and configure the broker. -A https://github.com/rabbitmq/hop[Java Binding for the API] is now provided. -The `com.rabbitmq.http.client.Client` is a standard, immediate, and, therefore, blocking API. -It is based on the https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#spring-web[Spring Web] module and its `RestTemplate` implementation. -On the other hand, the `com.rabbitmq.http.client.ReactorNettyClient` is a reactive, non-blocking implementation based on the https://projectreactor.io/docs/netty/release/reference/docs/index.html[Reactor Netty] project. - -The hop dependency (`com.rabbitmq:http-client`) is now also `optional`. - -See their Javadoc for more information. - -[[exception-handling]] -==== Exception Handling - -Many operations with the RabbitMQ Java client can throw checked exceptions. -For example, there are a lot of cases where `IOException` instances may be thrown. -The `RabbitTemplate`, `SimpleMessageListenerContainer`, and other Spring AMQP components catch those exceptions and convert them into one of the exceptions within `AmqpException` hierarchy. -Those are defined in the 'org.springframework.amqp' package, and `AmqpException` is the base of the hierarchy. - -When a listener throws an exception, it is wrapped in a `ListenerExecutionFailedException`. -Normally the message is rejected and requeued by the broker. -Setting `defaultRequeueRejected` to `false` causes messages to be discarded (or routed to a dead letter exchange). -As discussed in <>, the listener can throw an `AmqpRejectAndDontRequeueException` (or `ImmediateRequeueAmqpException`) to conditionally control this behavior. - -However, there is a class of errors where the listener cannot control the behavior. -When a message that cannot be converted is encountered (for example, an invalid `content_encoding` header), some exceptions are thrown before the message reaches user code. -With `defaultRequeueRejected` set to `true` (default) (or throwing an `ImmediateRequeueAmqpException`), such messages would be redelivered over and over. -Before version 1.3.2, users needed to write a custom `ErrorHandler`, as discussed in <>, to avoid this situation. - -Starting with version 1.3.2, the default `ErrorHandler` is now a `ConditionalRejectingErrorHandler` that rejects (and does not requeue) messages that fail with an irrecoverable error. -Specifically, it rejects messages that fail with the following errors: - -* `o.s.amqp...MessageConversionException`: Can be thrown when converting the incoming message payload using a `MessageConverter`. -* `o.s.messaging...MessageConversionException`: Can be thrown by the conversion service if additional conversion is required when mapping to a `@RabbitListener` method. -* `o.s.messaging...MethodArgumentNotValidException`: Can be thrown if validation (for example, `@Valid`) is used in the listener and the validation fails. -* `o.s.messaging...MethodArgumentTypeMismatchException`: Can be thrown if the inbound message was converted to a type that is not correct for the target method. -For example, the parameter is declared as `Message` but `Message` is received. -* `java.lang.NoSuchMethodException`: Added in version 1.6.3. -* `java.lang.ClassCastException`: Added in version 1.6.3. - -You can configure an instance of this error handler with a `FatalExceptionStrategy` so that users can provide their own rules for conditional message rejection -- for example, a delegate implementation to the `BinaryExceptionClassifier` from Spring Retry (<>). -In addition, the `ListenerExecutionFailedException` now has a `failedMessage` property that you can use in the decision. -If the `FatalExceptionStrategy.isFatal()` method returns `true`, the error handler throws an `AmqpRejectAndDontRequeueException`. -The default `FatalExceptionStrategy` logs a warning message when an exception is determined to be fatal. - -Since version 1.6.3, a convenient way to add user exceptions to the fatal list is to subclass `ConditionalRejectingErrorHandler.DefaultExceptionStrategy` and override the `isUserCauseFatal(Throwable cause)` method to return `true` for fatal exceptions. - -A common pattern for handling DLQ messages is to set a `time-to-live` on those messages as well as additional DLQ configuration such that these messages expire and are routed back to the main queue for retry. -The problem with this technique is that messages that cause fatal exceptions loop forever. -Starting with version 2.1, the `ConditionalRejectingErrorHandler` detects an `x-death` header on a message that causes a fatal exception to be thrown. -The message is logged and discarded. -You can revert to the previous behavior by setting the `discardFatalsWithXDeath` property on the `ConditionalRejectingErrorHandler` to `false`. - -IMPORTANT: Starting with version 2.1.9, messages with these fatal exceptions are rejected and NOT requeued by default, even if the container acknowledge mode is MANUAL. -These exceptions generally occur before the listener is invoked so the listener does not have a chance to ack or nack the message so it remained in the queue in an un-acked state. -To revert to the previous behavior, set the `rejectManual` property on the `ConditionalRejectingErrorHandler` to `false`. - -[[transactions]] -==== Transactions - -The Spring Rabbit framework has support for automatic transaction management in the synchronous and asynchronous use cases with a number of different semantics that can be selected declaratively, as is familiar to existing users of Spring transactions. -This makes many if not most common messaging patterns easy to implement. - -There are two ways to signal the desired transaction semantics to the framework. -In both the `RabbitTemplate` and `SimpleMessageListenerContainer`, there is a flag `channelTransacted` which, if `true`, tells the framework to use a transactional channel and to end all operations (send or receive) with a commit or rollback (depending on the outcome), with an exception signaling a rollback. -Another signal is to provide an external transaction with one of Spring's `PlatformTransactionManager` implementations as a context for the ongoing operation. -If there is already a transaction in progress when the framework is sending or receiving a message, and the `channelTransacted` flag is `true`, the commit or rollback of the messaging transaction is deferred until the end of the current transaction. -If the `channelTransacted` flag is `false`, no transaction semantics apply to the messaging operation (it is auto-acked). - -The `channelTransacted` flag is a configuration time setting. -It is declared and processed once when the AMQP components are created, usually at application startup. -The external transaction is more dynamic in principle because the system responds to the current thread state at runtime. -However, in practice, it is often also a configuration setting, when the transactions are layered onto an application declaratively. - -For synchronous use cases with `RabbitTemplate`, the external transaction is provided by the caller, either declaratively or imperatively according to taste (the usual Spring transaction model). -The following example shows a declarative approach (usually preferred because it is non-invasive), where the template has been configured with `channelTransacted=true`: - -==== -[source,java] ----- -@Transactional -public void doSomething() { - String incoming = rabbitTemplate.receiveAndConvert(); - // do some more database processing... - String outgoing = processInDatabaseAndExtractReply(incoming); - rabbitTemplate.convertAndSend(outgoing); -} ----- -==== - -In the preceding example, a `String` payload is received, converted, and sent as a message body inside a method marked as `@Transactional`. -If the database processing fails with an exception, the incoming message is returned to the broker, and the outgoing message is not sent. -This applies to any operations with the `RabbitTemplate` inside a chain of transactional methods (unless, for instance, the `Channel` is directly manipulated to commit the transaction early). - -For asynchronous use cases with `SimpleMessageListenerContainer`, if an external transaction is needed, it has to be requested by the container when it sets up the listener. -To signal that an external transaction is required, the user provides an implementation of `PlatformTransactionManager` to the container when it is configured. -The following example shows how to do so: - -==== -[source,java] ----- -@Configuration -public class ExampleExternalTransactionAmqpConfiguration { - - @Bean - public SimpleMessageListenerContainer messageListenerContainer() { - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); - container.setConnectionFactory(rabbitConnectionFactory()); - container.setTransactionManager(transactionManager()); - container.setChannelTransacted(true); - container.setQueueName("some.queue"); - container.setMessageListener(exampleListener()); - return container; - } - -} ----- -==== - -In the preceding example, the transaction manager is added as a dependency injected from another bean definition (not shown), and the `channelTransacted` flag is also set to `true`. -The effect is that if the listener fails with an exception, the transaction is rolled back, and the message is also returned to the broker. -Significantly, if the transaction fails to commit (for example, because of -a database constraint error or connectivity problem), the AMQP transaction is also rolled back, and the message is returned to the broker. -This is sometimes known as a "`Best Efforts 1 Phase Commit`", and is a very powerful pattern for reliable messaging. -If the `channelTransacted` flag was set to `false` (the default) in the preceding example, the external transaction would still be provided for the listener, but all messaging operations would be auto-acked, so the effect is to commit the messaging operations even on a rollback of the business operation. - -[[conditional-rollback]] -===== Conditional Rollback - -Prior to version 1.6.6, adding a rollback rule to a container's `transactionAttribute` when using an external transaction manager (such as JDBC) had no effect. -Exceptions always rolled back the transaction. - -Also, when using a https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/transaction.html#transaction-declarative[transaction advice] in the container's advice chain, conditional rollback was not very useful, because all listener exceptions are wrapped in a `ListenerExecutionFailedException`. - -The first problem has been corrected, and the rules are now applied properly. -Further, the `ListenerFailedRuleBasedTransactionAttribute` is now provided. -It is a subclass of `RuleBasedTransactionAttribute`, with the only difference being that it is aware of the `ListenerExecutionFailedException` and uses the cause of such exceptions for the rule. -This transaction attribute can be used directly in the container or through a transaction advice. - -The following example uses this rule: - -==== -[source, java] ----- -@Bean -public AbstractMessageListenerContainer container() { - ... - container.setTransactionManager(transactionManager); - RuleBasedTransactionAttribute transactionAttribute = - new ListenerFailedRuleBasedTransactionAttribute(); - transactionAttribute.setRollbackRules(Collections.singletonList( - new NoRollbackRuleAttribute(DontRollBackException.class))); - container.setTransactionAttribute(transactionAttribute); - ... -} ----- -==== - -[[transaction-rollback]] -===== A note on Rollback of Received Messages - -AMQP transactions apply only to messages and acks sent to the broker. -Consequently, when there is a rollback of a Spring transaction and a message has been received, Spring AMQP has to not only rollback the transaction but also manually reject the message (sort of a nack, but that is not what the specification calls it). -The action taken on message rejection is independent of transactions and depends on the `defaultRequeueRejected` property (default: `true`). -For more information about rejecting failed messages, see <>. - -For more information about RabbitMQ transactions and their limitations, see https://www.rabbitmq.com/semantics.html[RabbitMQ Broker Semantics]. - -NOTE: Prior to RabbitMQ 2.7.0, such messages (and any that are unacked when a channel is closed or aborts) went to the back of the queue on a Rabbit broker. -Since 2.7.0, rejected messages go to the front of the queue, in a similar manner to JMS rolled back messages. - -NOTE: Previously, message requeue on transaction rollback was inconsistent between local transactions and when a `TransactionManager` was provided. -In the former case, the normal requeue logic (`AmqpRejectAndDontRequeueException` or `defaultRequeueRejected=false`) applied (see <>). -With a transaction manager, the message was unconditionally requeued on rollback. -Starting with version 2.0, the behavior is consistent and the normal requeue logic is applied in both cases. -To revert to the previous behavior, you can set the container's `alwaysRequeueWithTxManagerRollback` property to `true`. -See <>. - -===== Using `RabbitTransactionManager` - -The https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.html[RabbitTransactionManager] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. -This transaction manager is an implementation of the https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/transaction/PlatformTransactionManager.html[`PlatformTransactionManager`] interface and should be used with a single Rabbit `ConnectionFactory`. - -IMPORTANT: This strategy is not able to provide XA transactions -- for example, in order to share transactions between messaging and database access. - -Application code is required to retrieve the transactional Rabbit resources through `ConnectionFactoryUtils.getTransactionalResourceHolder(ConnectionFactory, boolean)` instead of a standard `Connection.createChannel()` call with subsequent channel creation. -When using Spring AMQP's https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/core/RabbitTemplate.html[RabbitTemplate], it will autodetect a thread-bound Channel and automatically participate in its transaction. - -With Java Configuration, you can setup a new RabbitTransactionManager by using the following bean: - -==== -[source,java] ----- -@Bean -public RabbitTransactionManager rabbitTransactionManager() { - return new RabbitTransactionManager(connectionFactory); -} ----- -==== - -If you prefer XML configuration, you can declare the following bean in your XML Application Context file: - -==== -[source,xml] ----- - - - ----- -==== - -[[tx-sync]] -===== Transaction Synchronization - -Synchronizing a RabbitMQ transaction with some other (e.g. DBMS) transaction provides "Best Effort One Phase Commit" semantics. -It is possible that the RabbitMQ transaction fails to commit during the after completion phase of transaction synchronization. -This is logged by the `spring-tx` infrastructure as an error, but no exception is thrown to the calling code. -Starting with version 2.3.10, you can call `ConnectionUtils.checkAfterCompletion()` after the transaction has committed on the same thread that processed the transaction. -It will simply return if no exception occurred; otherwise it will throw an `AfterCompletionFailedException` which will have a property representing the synchronization status of the completion. - -Enable this feature by calling `ConnectionFactoryUtils.enableAfterCompletionFailureCapture(true)`; this is a global flag and applies to all threads. - -[[containerAttributes]] -==== Message Listener Container Configuration - -There are quite a few options for configuring a `SimpleMessageListenerContainer` (SMLC) and a `DirectMessageListenerContainer` (DMLC) related to transactions and quality of service, and some of them interact with each other. -Properties that apply to the SMLC or DMLC are indicated by the check mark in the appropriate column. -See <> for information to help you decide which container is appropriate for your application. - -The following table shows the container property names and their equivalent attribute names (in parentheses) when using the namespace to configure a ``. -The `type` attribute on that element can be `simple` (default) or `direct` to specify an `SMLC` or `DMLC` respectively. -Some properties are not exposed by the namespace. -These are indicated by `N/A` for the attribute. - -.Configuration options for a message listener container -[cols="6l,16,1,1", options="header"] -|=== -|Property -(Attribute) -|Description -|SMLC -|DMLC - -|ackTimeout -(N/A) - -|When `messagesPerAck` is set, this timeout is used as an alternative to send an ack. -When a new message arrives, the count of unacked messages is compared to `messagesPerAck`, and the time since the last ack is compared to this value. -If either condition is `true`, the message is acknowledged. -When no new messages arrive and there are unacked messages, this timeout is approximate since the condition is only checked each `monitorInterval`. -See also `messagesPerAck` and `monitorInterval` in this table. - -a| -a|image::images/tickmark.png[] - -|acknowledgeMode -(acknowledge) - -a| -* `NONE`: No acks are sent (incompatible with `channelTransacted=true`). -RabbitMQ calls this "`autoack`", because the broker assumes all messages are acked without any action from the consumer. -* `MANUAL`: The listener must acknowledge all messages by calling `Channel.basicAck()`. -* `AUTO`: The container acknowledges the message automatically, unless the `MessageListener` throws an exception. -Note that `acknowledgeMode` is complementary to `channelTransacted` -- if the channel is transacted, the broker requires a commit notification in addition to the ack. -This is the default mode. -See also `batchSize`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|adviceChain -(advice-chain) - -|An array of AOP Advice to apply to the listener execution. -This can be used to apply additional cross-cutting concerns, such as automatic retry in the event of broker death. -Note that simple re-connection after an AMQP error is handled by the `CachingConnectionFactory`, as long as the broker is still alive. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|afterReceivePostProcessors -(N/A) - -|An array of `MessagePostProcessor` instances that are invoked before invoking the listener. -Post processors can implement `PriorityOrdered` or `Ordered`. -The array is sorted with un-ordered members invoked last. -If a post processor returns `null`, the message is discarded (and acknowledged, if appropriate). - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|alwaysRequeueWith -TxManagerRollback -(N/A) - -|Set to `true` to always requeue messages on rollback when a transaction manager is configured. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|autoDeclare -(auto-declare) - -a|When set to `true` (default), the container uses a `RabbitAdmin` to redeclare all AMQP objects (queues, exchanges, bindings), if it detects that at least one of its queues is missing during startup, perhaps because it is an `auto-delete` or an expired queue, but the redeclaration proceeds if the queue is missing for any reason. -To disable this behavior, set this property to `false`. -Note that the container fails to start if all of its queues are missing. - -NOTE: Prior to version 1.6, if there was more than one admin in the context, the container would randomly select one. -If there were no admins, it would create one internally. -In either case, this could cause unexpected results. -Starting with version 1.6, for `autoDeclare` to work, there must be exactly one `RabbitAdmin` in the context, or a reference to a specific instance must be configured on the container using the `rabbitAdmin` property. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|autoStartup -(auto-startup) - -|Flag to indicate that the container should start when the `ApplicationContext` does (as part of the `SmartLifecycle` callbacks, which happen after all beans are initialized). -Defaults to `true`, but you can set it to `false` if your broker might not be available on startup and call `start()` later manually when you know the broker is ready. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|batchSize -(transaction-size) -(batch-size) - -|When used with `acknowledgeMode` set to `AUTO`, the container tries to process up to this number of messages before sending an ack (waiting for each one up to the receive timeout setting). -This is also when a transactional channel is committed. -If the `prefetchCount` is less than the `batchSize`, it is increased to match the `batchSize`. - -a|image::images/tickmark.png[] -a| - -|batchingStrategy -(N/A) - -|The strategy used when debatchng messages. -Default `SimpleDebatchingStrategy`. -See <> and <>. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|channelTransacted -(channel-transacted) - -|Boolean flag to signal that all messages should be acknowledged in a transaction (either manually or automatically). - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|concurrency -(N/A) - -|`m-n` The range of concurrent consumers for each listener (min, max). -If only `n` is provided, `n` is a fixed number of consumers. -See <>. - -a|image::images/tickmark.png[] -a| - -|concurrentConsumers -(concurrency) - -|The number of concurrent consumers to initially start for each listener. -See <>. - -a|image::images/tickmark.png[] -a| - -|connectionFactory -(connection-factory) - -|A reference to the `ConnectionFactory`. -When configuring byusing the XML namespace, the default referenced bean name is `rabbitConnectionFactory`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|consecutiveActiveTrigger -(min-consecutive-active) - -|The minimum number of consecutive messages received by a consumer, without a receive timeout occurring, when considering starting a new consumer. -Also impacted by 'batchSize'. -See <>. -Default: 10. - -a|image::images/tickmark.png[] -a| - -|consecutiveIdleTrigger -(min-consecutive-idle) - -|The minimum number of receive timeouts a consumer must experience before considering stopping a consumer. -Also impacted by 'batchSize'. -See <>. -Default: 10. - -a|image::images/tickmark.png[] -a| - -|consumerBatchEnabled -(batch-enabled) - -|If the `MessageListener` supports it, setting this to true enables batching of discrete messages, up to `batchSize`; a partial batch will be delivered if no new messages arrive in `receiveTimeout`. -When this is false, batching is only supported for batches created by a producer; see <>. - -a|image::images/tickmark.png[] -a| - -|consumerStartTimeout -(N/A) - -|The time in milliseconds to wait for a consumer thread to start. -If this time elapses, an error log is written. -An example of when this might happen is if a configured `taskExecutor` has insufficient threads to support the container `concurrentConsumers`. - -See <>. -Default: 60000 (one minute). - -a|image::images/tickmark.png[] -a| - -|consumerTagStrategy -(consumer-tag-strategy) - -|Set an implementation of <>, enabling the creation of a (unique) tag for each consumer. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|consumersPerQueue -(consumers-per-queue) - -|The number of consumers to create for each configured queue. -See <>. - -a| -a|image::images/tickmark.png[] - -|consumeDelay -(N/A) - -|When using the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `concurrentConsumers > 1`, there is a race condition that can prevent even distribution of the consumers across the shards. -Use this property to add a small delay between consumer starts to avoid this race condition. -You should experiment with values to determine the suitable delay for your environment. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|debatchingEnabled -(N/A) - -|When true, the listener container will debatch batched messages and invoke the listener with each message from the batch. -Starting with version 2.2.7, <> will be debatched as a `List` if the listener is a `BatchMessageListener` or `ChannelAwareBatchMessageListener`. -Otherwise messages from the batch are presented one-at-a-time. -Default true. -See <> and <>. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|declarationRetries -(declaration-retries) - -|The number of retry attempts when passive queue declaration fails. -Passive queue declaration occurs when the consumer starts or, when consuming from multiple queues, when not all queues were available during initialization. -When none of the configured queues can be passively declared (for any reason) after the retries are exhausted, the container behavior is controlled by the 'missingQueuesFatal` property, described earlier. -Default: Three retries (for a total of four attempts). - -a|image::images/tickmark.png[] -a| - -|defaultRequeueRejected -(requeue-rejected) - -|Determines whether messages that are rejected because the listener threw an exception should be requeued or not. -Default: `true`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|errorHandler -(error-handler) - -|A reference to an `ErrorHandler` strategy for handling any uncaught exceptions that may occur during the execution of the MessageListener. -Default: `ConditionalRejectingErrorHandler` - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|exclusive -(exclusive) - -|Determines whether the single consumer in this container has exclusive access to the queues. -The concurrency of the container must be 1 when this is `true`. -If another consumer has exclusive access, the container tries to recover the consumer, according to the -`recovery-interval` or `recovery-back-off`. -When using the namespace, this attribute appears on the `` element along with the queue names. -Default: `false`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|exclusiveConsumer -ExceptionLogger -(N/A) - -|An exception logger used when an exclusive consumer cannot gain access to a queue. -By default, this is logged at the `WARN` level. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|failedDeclaration -RetryInterval -(failed-declaration --retry-interval) - -|The interval between passive queue declaration retry attempts. -Passive queue declaration occurs when the consumer starts or, when consuming from multiple queues, when not all queues were available during initialization. -Default: 5000 (five seconds). - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|forceCloseChannel -(N/A) - -|If the consumers do not respond to a shutdown within `shutdownTimeout`, if this is `true`, the channel will be closed, causing any unacked messages to be requeued. -Defaults to `true` since 2.0. -You can set it to `false` to revert to the previous behavior. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|globalQos -(global-qos) - -|When true, the `prefetchCount` is applied globally to the channel rather than to each consumer on the channel. -See https://www.rabbitmq.com/amqp-0-9-1-reference.html#basic.qos.global[`basicQos.global`] for more information. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|(group) - -|This is available only when using the namespace. -When specified, a bean of type `Collection` is registered with this name, and the -container for each `` element is added to the collection. -This allows, for example, starting and stopping the group of containers by iterating over the collection. -If multiple `` elements have the same group value, the containers in the collection form -an aggregate of all containers so designated. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|idleEventInterval -(idle-event-interval) - -|See <>. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|javaLangErrorHandler -(N/A) - -|An `AbstractMessageListenerContainer.JavaLangErrorHandler` implementation that is called when a container thread catches an `Error`. -The default implementation calls `System.exit(99)`; to revert to the previous behavior (do nothing), add a no-op handler. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|maxConcurrentConsumers -(max-concurrency) - -|The maximum number of concurrent consumers to start, if needed, on demand. -Must be greater than or equal to 'concurrentConsumers'. -See <>. - -a|image::images/tickmark.png[] -a| - -|messagesPerAck -(N/A) - -|The number of messages to receive between acks. -Use this to reduce the number of acks sent to the broker (at the cost of increasing the possibility of redelivered messages). -Generally, you should set this property only on high-volume listener containers. -If this is set and a message is rejected (exception thrown), pending acks are acknowledged and the failed message is rejected. -Not allowed with transacted channels. -If the `prefetchCount` is less than the `messagesPerAck`, it is increased to match the `messagesPerAck`. -Default: ack every message. -See also `ackTimeout` in this table. - -a| -a|image::images/tickmark.png[] - -|mismatchedQueuesFatal -(mismatched-queues-fatal) - -a|When the container starts, if this property is `true` (default: `false`), the container checks that all queues declared in the context are compatible with queues already on the broker. -If mismatched properties (such as `auto-delete`) or arguments (skuch as `x-message-ttl`) exist, the container (and application context) fails to start with a fatal exception. - -If the problem is detected during recovery (for example, after a lost connection), the container is stopped. - -There must be a single `RabbitAdmin` in the application context (or one specifically configured on the container by using the `rabbitAdmin` property). -Otherwise, this property must be `false`. - -NOTE: If the broker is not available during initial startup, the container starts and the conditions are checked when the connection is established. - -IMPORTANT: The check is done against all queues in the context, not just the queues that a particular listener is configured to use. -If you wish to limit the checks to just those queues used by a container, you should configure a separate `RabbitAdmin` for the container, and provide a reference to it using the `rabbitAdmin` property. -See <> for more information. - -IMPORTANT: Mismatched queue argument detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`. -This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds. -Applications using lazy listener beans should check the queue arguments before getting a reference to the lazy bean. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|missingQueuesFatal -(missing-queues-fatal) - -a|When set to `true` (default), if none of the configured queues are available on the broker, it is considered fatal. -This causes the application context to fail to initialize during startup. -Also, when the queues are deleted while the container is running, by default, the consumers make three retries to connect to the queues (at five second intervals) and stop the container if these attempts fail. - -This was not configurable in previous versions. - -When set to `false`, after making the three retries, the container goes into recovery mode, as with other problems, such as the broker being down. -The container tries to recover according to the `recoveryInterval` property. -During each recovery attempt, each consumer again tries four times to passively declare the queues at five second intervals. -This process continues indefinitely. - -You can also use a properties bean to set the property globally for all containers, as follows: - -==== -[source,xml] ----- - - - false - - ----- -==== - -This global property is not applied to any containers that have an explicit `missingQueuesFatal` property set. - -The default retry properties (three retries at five-second intervals) can be overridden by setting the properties below. - -IMPORTANT: Missing queue detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`. -This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds. -Applications using lazy listener beans should check the queue(s) before getting a reference to the lazy bean. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|monitorInterval -(monitor-interval) - -|With the DMLC, a task is scheduled to run at this interval to monitor the state of the consumers and recover any that have failed. - -a| -a|image::images/tickmark.png[] - -|noLocal -(N/A) - -|Set to `true` to disable delivery from the server to consumers messages published on the same channel's connection. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|phase -(phase) - -|When `autoStartup` is `true`, the lifecycle phase within which this container should start and stop. -The lower the value, the earlier this container starts and the later it stops. -The default is `Integer.MAX_VALUE`, meaning the container starts as late as possible and stops as soon as possible. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|possibleAuthentication -FailureFatal -(possible-authentication- -failure-fatal) - -a|When set to `true` (default for SMLC), if a `PossibleAuthenticationFailureException` is thrown during connection, it is considered fatal. -This causes the application context to fail to initialize during startup (if the container is configured with auto startup). - -Since _version 2.0_. - -**DirectMessageListenerContainer** - -When set to `false` (default), each consumer will attempt to reconnect according to the `monitorInterval`. - -**SimpleMessageListenerContainer** - -When set to `false`, after making the 3 retries, the container will go into recovery mode, as with other problems, such as the broker being down. -The container will attempt to recover according to the `recoveryInterval` property. -During each recovery attempt, each consumer will again try 4 times to start. -This process will continue indefinitely. - -You can also use a properties bean to set the property globally for all containers, as follows: - -[source,xml] ----- - - - false - - ----- - -This global property will not be applied to any containers that have an explicit `missingQueuesFatal` property set. - -The default retry properties (3 retries at 5 second intervals) can be overridden using the properties after this one. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|prefetchCount -(prefetch) - -a|The number of unacknowledged messages that can be outstanding at each consumer. -The higher this value is, the faster the messages can be delivered, but the higher the risk of non-sequential processing. -Ignored if the `acknowledgeMode` is `NONE`. -This is increased, if necessary, to match the `batchSize` or `messagePerAck`. -Defaults to 250 since 2.0. -You can set it to 1 to revert to the previous behavior. - -IMPORTANT: There are scenarios where the prefetch value should -be low -- for example, with large messages, especially if the processing is slow (messages could add up -to a large amount of memory in the client process), and if strict message ordering is necessary -(the prefetch value should be set back to 1 in this case). -Also, with low-volume messaging and multiple consumers (including concurrency within a single listener container instance), you may wish to reduce the prefetch to get a more even distribution of messages across consumers. - -Also see `globalQos`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|rabbitAdmin -(admin) - -|When a listener container listens to at least one auto-delete queue and it is found to be missing during startup, the container uses a `RabbitAdmin` to declare the queue and any related bindings and exchanges. -If such elements are configured to use conditional declaration (see <>), the container must use the admin that was configured to declare those elements. -Specify that admin here. -It is required only when using auto-delete queues with conditional declaration. -If you do not wish the auto-delete queues to be declared until the container is started, set `auto-startup` to `false` on the admin. -Defaults to a `RabbitAdmin` that declares all non-conditional elements. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|receiveTimeout -(receive-timeout) - -|The maximum time to wait for each message. -If `acknowledgeMode=NONE`, this has very little effect -- the container spins round and asks for another message. -It has the biggest effect for a transactional `Channel` with `batchSize > 1`, since it can cause messages already consumed not to be acknowledged until the timeout expires. -When `consumerBatchEnabled` is true, a partial batch will be delivered if this timeout occurs before a batch is complete. - -a|image::images/tickmark.png[] -a| - -|recoveryBackOff -(recovery-back-off) - -|Specifies the `BackOff` for intervals between attempts to start a consumer if it fails to start for non-fatal reasons. -Default is `FixedBackOff` with unlimited retries every five seconds. -Mutually exclusive with `recoveryInterval`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|recoveryInterval -(recovery-interval) - -|Determines the time in milliseconds between attempts to start a consumer if it fails to start for non-fatal reasons. -Default: 5000. -Mutually exclusive with `recoveryBackOff`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|retryDeclarationInterval -(missing-queue- -retry-interval) - -|If a subset of the configured queues are available during consumer initialization, the consumer starts consuming from those queues. -The consumer tries to passively declare the missing queues by using this interval. -When this interval elapses, the 'declarationRetries' and 'failedDeclarationRetryInterval' is used again. -If there are still missing queues, the consumer again waits for this interval before trying again. -This process continues indefinitely until all queues are available. -Default: 60000 (one minute). - -a|image::images/tickmark.png[] -a| - -|shutdownTimeout -(N/A) - -|When a container shuts down (for example, -if its enclosing `ApplicationContext` is closed), it waits for in-flight messages to be processed up to this limit. -Defaults to five seconds. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|startConsumerMinInterval -(min-start-interval) - -|The time in milliseconds that must elapse before each new consumer is started on demand. -See <>. -Default: 10000 (10 seconds). - -a|image::images/tickmark.png[] -a| - -|statefulRetryFatal -WithNullMessageId -(N/A) - -|When using a stateful retry advice, if a message with a missing `messageId` property is received, it is considered -fatal for the consumer (it is stopped) by default. -Set this to `false` to discard (or route to a dead-letter queue) such messages. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|stopConsumerMinInterval -(min-stop-interval) - -|The time in milliseconds that must elapse before a consumer is stopped since the last consumer was stopped when an idle consumer is detected. -See <>. -Default: 60000 (one minute). - -a|image::images/tickmark.png[] -a| - -|taskExecutor -(task-executor) - -|A reference to a Spring `TaskExecutor` (or standard JDK 1.5+ `Executor`) for executing listener invokers. -Default is a `SimpleAsyncTaskExecutor`, using internally managed threads. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|taskScheduler -(task-scheduler) - -|With the DMLC, the scheduler used to run the monitor task at the 'monitorInterval'. - -a| -a|image::images/tickmark.png[] - -|transactionManager -(transaction-manager) - -|External transaction manager for the operation of the listener. -Also complementary to `channelTransacted` -- if the `Channel` is transacted, its transaction is synchronized with the external transaction. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|=== - -[[listener-concurrency]] -==== Listener Concurrency - -===== SimpleMessageListenerContainer - -By default, the listener container starts a single consumer that receives messages from the queues. - -When examining the table in the previous section, you can see a number of properties and attributes that control concurrency. -The simplest is `concurrentConsumers`, which creates that (fixed) number of consumers that concurrently process messages. - -Prior to version 1.3.0, this was the only setting available and the container had to be stopped and started again to change the setting. - -Since version 1.3.0, you can now dynamically adjust the `concurrentConsumers` property. -If it is changed while the container is running, consumers are added or removed as necessary to adjust to the new setting. - -In addition, a new property called `maxConcurrentConsumers` has been added and the container dynamically adjusts the concurrency based on workload. -This works in conjunction with four additional properties: `consecutiveActiveTrigger`, `startConsumerMinInterval`, `consecutiveIdleTrigger`, and `stopConsumerMinInterval`. -With the default settings, the algorithm to increase consumers works as follows: - -If the `maxConcurrentConsumers` has not been reached and an existing consumer is active for ten consecutive cycles AND at least 10 seconds has elapsed since the last consumer was started, a new consumer is started. -A consumer is considered active if it received at least one message in `batchSize` * `receiveTimeout` milliseconds. - -With the default settings, the algorithm to decrease consumers works as follows: - -If there are more than `concurrentConsumers` running and a consumer detects ten consecutive timeouts (idle) AND the last consumer was stopped at least 60 seconds ago, a consumer is stopped. -The timeout depends on the `receiveTimeout` and the `batchSize` properties. -A consumer is considered idle if it receives no messages in `batchSize` * `receiveTimeout` milliseconds. -So, with the default timeout (one second) and a `batchSize` of four, stopping a consumer is considered after 40 seconds of idle time (four timeouts correspond to one idle detection). - -NOTE: Practically, consumers can be stopped only if the whole container is idle for some time. -This is because the broker shares its work across all the active consumers. - -Each consumer uses a single channel, regardless of the number of configured queues. - -Starting with version 2.0, the `concurrentConsumers` and `maxConcurrentConsumers` properties can be set with the `concurrency` property -- for example, `2-4`. - -===== Using `DirectMessageListenerContainer` - -With this container, concurrency is based on the configured queues and `consumersPerQueue`. -Each consumer for each queue uses a separate channel, and the concurrency is controlled by the rabbit client library. -By default, at the time of writing, it uses a pool of `DEFAULT_NUM_THREADS = Runtime.getRuntime().availableProcessors() * 2` threads. - -You can configure a `taskExecutor` to provide the required maximum concurrency. - -[[exclusive-consumer]] -==== Exclusive Consumer - -Starting with version 1.3, you can configure the listener container with a single exclusive consumer. -This prevents other containers from consuming from the queues until the current consumer is cancelled. -The concurrency of such a container must be `1`. - -When using exclusive consumers, other containers try to consume from the queues according to the `recoveryInterval` property and log a `WARN` message if the attempt fails. - -[[listener-queues]] -==== Listener Container Queues - -Version 1.3 introduced a number of improvements for handling multiple queues in a listener container. - -The container must be configured to listen on at least one queue. -This was the case previously, too, but now queues can be added and removed at runtime. -The container recycles (cancels and re-creates) the consumers when any pre-fetched messages have been processed. -See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.html[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. -When removing queues, at least one queue must remain. - -A consumer now starts if any of its queues are available. -Previously, the container would stop if any queues were unavailable. -Now, this is only the case if none of the queues are available. -If not all queues are available, the container tries to passively declare (and consume from) the missing queues every 60 seconds. - -Also, if a consumer receives a cancel from the broker (for example, if a queue is deleted) the consumer tries to recover, and the recovered consumer continues to process messages from any other configured queues. -Previously, a cancel on one queue cancelled the entire consumer and, eventually, the container would stop due to the missing queue. - -If you wish to permanently remove a queue, you should update the container before or after deleting to queue, to avoid future attempts trying to consume from it. - -==== Resilience: Recovering from Errors and Broker Failures - -Some of the key (and most popular) high-level features that Spring AMQP provides are to do with recovery and automatic re-connection in the event of a protocol error or broker failure. -We have seen all the relevant components already in this guide, but it should help to bring them all together here and call out the features and recovery scenarios individually. - -The primary reconnection features are enabled by the `CachingConnectionFactory` itself. -It is also often beneficial to use the `RabbitAdmin` auto-declaration features. -In addition, if you care about guaranteed delivery, you probably also need to use the `channelTransacted` flag in `RabbitTemplate` and `SimpleMessageListenerContainer` and the `AcknowledgeMode.AUTO` (or manual if you do the acks yourself) in the `SimpleMessageListenerContainer`. - -[[automatic-declaration]] -===== Automatic Declaration of Exchanges, Queues, and Bindings - -The `RabbitAdmin` component can declare exchanges, queues, and bindings on startup. -It does this lazily, through a `ConnectionListener`. -Consequently, if the broker is not present on startup, it does not matter. -The first time a `Connection` is used (for example, -by sending a message) the listener fires and the admin features is applied. -A further benefit of doing the auto declarations in a listener is that, if the connection is dropped for any reason (for example, -broker death, network glitch, and others), they are applied again when the connection is re-established. - -NOTE: Queues declared this way must have fixed names -- either explicitly declared or generated by the framework for `AnonymousQueue` instances. -Anonymous queues are non-durable, exclusive, and auto-deleting. - -IMPORTANT: Automatic declaration is performed only when the `CachingConnectionFactory` cache mode is `CHANNEL` (the default). -This limitation exists because exclusive and auto-delete queues are bound to the connection. - -Starting with version 2.2.2, the `RabbitAdmin` will detect beans of type `DeclarableCustomizer` and apply the function before actually processing the declaration. -This is useful, for example, to set a new argument (property) before it has first class support within the framework. - -==== -[source, java] ----- -@Bean -public DeclarableCustomizer customizer() { - return dec -> { - if (dec instanceof Queue && ((Queue) dec).getName().equals("my.queue")) { - dec.addArgument("some.new.queue.argument", true); - } - return dec; - }; -} ----- -==== - -It is also useful in projects that don't provide direct access to the `Declarable` bean definitions. - -See also <>. - -[[retry]] -===== Failures in Synchronous Operations and Options for Retry - -If you lose your connection to the broker in a synchronous sequence when using `RabbitTemplate` (for instance), Spring AMQP throws an `AmqpException` (usually, but not always, `AmqpIOException`). -We do not try to hide the fact that there was a problem, so you have to be able to catch and respond to the exception. -The easiest thing to do if you suspect that the connection was lost (and it was not your fault) is to try the operation again. -You can do this manually, or you could look at using Spring Retry to handle the retry (imperatively or declaratively). - -Spring Retry provides a couple of AOP interceptors and a great deal of flexibility to specify the parameters of the retry (number of attempts, exception types, backoff algorithm, and others). -Spring AMQP also provides some convenience factory beans for creating Spring Retry interceptors in a convenient form for AMQP use cases, with strongly typed callback interfaces that you can use to implement custom recovery logic. -See the Javadoc and properties of `StatefulRetryOperationsInterceptor` and `StatelessRetryOperationsInterceptor` for more detail. -Stateless retry is appropriate if there is no transaction or if a transaction is started inside the retry callback. -Note that stateless retry is simpler to configure and analyze than stateful retry, but it is not usually appropriate if there is an ongoing transaction that must be rolled back or definitely is going to roll back. -A dropped connection in the middle of a transaction should have the same effect as a rollback. -Consequently, for reconnections where the transaction is started higher up the stack, stateful retry is usually the best choice. -Stateful retry needs a mechanism to uniquely identify a message. -The simplest approach is to have the sender put a unique value in the `MessageId` message property. -The provided message converters provide an option to do this: you can set `createMessageIds` to `true`. -Otherwise, you can inject a `MessageKeyGenerator` implementation into the interceptor. -The key generator must return a unique key for each message. -In versions prior to version 2.0, a `MissingMessageIdAdvice` was provided. -It enabled messages without a `messageId` property to be retried exactly once (ignoring the retry settings). -This advice is no longer provided, since, along with `spring-retry` version 1.2, its functionality is built into the interceptor and message listener containers. - -NOTE: For backwards compatibility, a message with a null message ID is considered fatal for the consumer (consumer is stopped) by default (after one retry). -To replicate the functionality provided by the `MissingMessageIdAdvice`, you can set the `statefulRetryFatalWithNullMessageId` property to `false` on the listener container. -With that setting, the consumer continues to run and the message is rejected (after one retry). -It is discarded or routed to the dead letter queue (if one is configured). - -Starting with version 1.3, a builder API is provided to aid in assembling these interceptors by using Java (in `@Configuration` classes). -The following example shows how to do so: - -==== -[source,java] ----- -@Bean -public StatefulRetryOperationsInterceptor interceptor() { - return RetryInterceptorBuilder.stateful() - .maxAttempts(5) - .backOffOptions(1000, 2.0, 10000) // initialInterval, multiplier, maxInterval - .build(); -} ----- -==== - -Only a subset of retry capabilities can be configured this way. -More advanced features would need the configuration of a `RetryTemplate` as a Spring bean. -See the https://docs.spring.io/spring-retry/docs/api/current/[Spring Retry Javadoc] for complete information about available policies and their configuration. - -[[batch-retry]] -===== Retry with Batch Listeners - -It is not recommended to configure retry with a batch listener, unless the batch was created by the producer, in a single record. -See <> for information about consumer and producer-created batches. -With a consumer-created batch, the framework has no knowledge about which message in the batch caused the failure so recovery after the retries are exhausted is not possible. -With producer-created batches, since there is only one message that actually failed, the whole message can be recovered. -Applications may want to inform a custom recoverer where in the batch the failure occurred, perhaps by setting an index property of the thrown exception. - -A retry recoverer for a batch listener must implement `MessageBatchRecoverer`. - -[[async-listeners]] -===== Message Listeners and the Asynchronous Case - -If a `MessageListener` fails because of a business exception, the exception is handled by the message listener container, which then goes back to listening for another message. -If the failure is caused by a dropped connection (not a business exception), the consumer that is collecting messages for the listener has to be cancelled and restarted. -The `SimpleMessageListenerContainer` handles this seamlessly, and it leaves a log to say that the listener is being restarted. -In fact, it loops endlessly, trying to restart the consumer. -Only if the consumer is very badly behaved indeed will it give up. -One side effect is that if the broker is down when the container starts, it keeps trying until a connection can be established. - -Business exception handling, as opposed to protocol errors and dropped connections, might need more thought and some custom configuration, especially if transactions or container acks are in use. -Prior to 2.8.x, RabbitMQ had no definition of dead letter behavior. -Consequently, by default, a message that is rejected or rolled back because of a business exception can be redelivered endlessly. -To put a limit on the client on the number of re-deliveries, one choice is a `StatefulRetryOperationsInterceptor` in the advice chain of the listener. -The interceptor can have a recovery callback that implements a custom dead letter action -- whatever is appropriate for your particular environment. - -Another alternative is to set the container's `defaultRequeueRejected` property to `false`. -This causes all failed messages to be discarded. -When using RabbitMQ 2.8.x or higher, this also facilitates delivering the message to a dead letter exchange. - -Alternatively, you can throw a `AmqpRejectAndDontRequeueException`. -Doing so prevents message requeuing, regardless of the setting of the `defaultRequeueRejected` property. - -Starting with version 2.1, an `ImmediateRequeueAmqpException` is introduced to perform exactly the opposite logic: the message will be requeued, regardless of the setting of the `defaultRequeueRejected` property. - -Often, a combination of both techniques is used. -You can use a `StatefulRetryOperationsInterceptor` in the advice chain with a `MessageRecoverer` that throws an `AmqpRejectAndDontRequeueException`. -The `MessageRecover` is called when all retries have been exhausted. -The `RejectAndDontRequeueRecoverer` does exactly that. -The default `MessageRecoverer` consumes the errant message and emits a `WARN` message. - -Starting with version 1.3, a new `RepublishMessageRecoverer` is provided, to allow publishing of failed messages after retries are exhausted. - -When a recoverer consumes the final exception, the message is ack'd and is not sent to the dead letter exchange, if any. - -NOTE: When `RepublishMessageRecoverer` is used on the consumer side, the received message has `deliveryMode` in the `receivedDeliveryMode` message property. -In this case the `deliveryMode` is `null`. -That means a `NON_PERSISTENT` delivery mode on the broker. -Starting with version 2.0, you can configure the `RepublishMessageRecoverer` for the `deliveryMode` to set into the message to republish if it is `null`. -By default, it uses `MessageProperties` default value - `MessageDeliveryMode.PERSISTENT`. - -The following example shows how to set a `RepublishMessageRecoverer` as the recoverer: - -==== -[source,java] ----- -@Bean -RetryOperationsInterceptor interceptor() { - return RetryInterceptorBuilder.stateless() - .maxAttempts(5) - .recoverer(new RepublishMessageRecoverer(amqpTemplate(), "something", "somethingelse")) - .build(); -} ----- -==== - -The `RepublishMessageRecoverer` publishes the message with additional information in message headers, such as the exception message, stack trace, original exchange, and routing key. -Additional headers can be added by creating a subclass and overriding `additionalHeaders()`. -The `deliveryMode` (or any other properties) can also be changed in the `additionalHeaders()`, as the following example shows: - -==== -[source,java] ----- -RepublishMessageRecoverer recoverer = new RepublishMessageRecoverer(amqpTemplate, "error") { - - protected Map additionalHeaders(Message message, Throwable cause) { - message.getMessageProperties() - .setDeliveryMode(message.getMessageProperties().getReceivedDeliveryMode()); - return null; - } - -}; ----- -==== - -Starting with version 2.0.5, the stack trace may be truncated if it is too large; this is because all headers have to fit in a single frame. -By default, if the stack trace would cause less than 20,000 bytes ('headroom') to be available for other headers, it will be truncated. -This can be adjusted by setting the recoverer's `frameMaxHeadroom` property, if you need more or less space for other headers. -Starting with versions 2.1.13, 2.2.3, the exception message is included in this calculation, and the amount of stack trace will be maximized using the following algorithm: - -* if the stack trace alone would exceed the limit, the exception message header will be truncated to 97 bytes plus `...` and the stack trace is truncated too. -* if the stack trace is small, the message will be truncated (plus `...`) to fit in the available bytes (but the message within the stack trace itself is truncated to 97 bytes plus `...`). - -Whenever a truncation of any kind occurs, the original exception will be logged to retain the complete information. - -Starting with version 2.3.3, a new subclass `RepublishMessageRecovererWithConfirms` is provided; this supports both styles of publisher confirms and will wait for the confirmation before returning (or throw an exception if not confirmed or the message is returned). - -If the confirm type is `CORRELATED`, the subclass will also detect if a message is returned and throw an `AmqpMessageReturnedException`; if the publication is negatively acknowledged, it will throw an `AmqpNackReceivedException`. - -If the confirm type is `SIMPLE`, the subclass will invoke the `waitForConfirmsOrDie` method on the channel. - -See <> for more information about confirms and returns. - -Starting with version 2.1, an `ImmediateRequeueMessageRecoverer` is added to throw an `ImmediateRequeueAmqpException`, which notifies a listener container to requeue the current failed message. - -===== Exception Classification for Spring Retry - -Spring Retry has a great deal of flexibility for determining which exceptions can invoke retry. -The default configuration retries for all exceptions. -Given that user exceptions are wrapped in a `ListenerExecutionFailedException`, we need to ensure that the classification examines the exception causes. -The default classifier looks only at the top level exception. - -Since Spring Retry 1.0.3, the `BinaryExceptionClassifier` has a property called `traverseCauses` (default: `false`). -When `true`, it travers exception causes until it finds a match or there is no cause. - -To use this classifier for retry, you can use a `SimpleRetryPolicy` created with the constructor that takes the max attempts, the `Map` of `Exception` instances, and the boolean (`traverseCauses`) and inject this policy into the `RetryTemplate`. - -[[multi-rabbit]] -==== Multiple Broker (or Cluster) Support - -Version 2.3 added more convenience when communicating between a single application and multiple brokers or broker clusters. -The main benefit, on the consumer side, is that the infrastructure can automatically associate auto-declared queues with the appropriate broker. - -This is best illustrated with an example: - -==== -[source, java] ----- -@SpringBootApplication(exclude = RabbitAutoConfiguration.class) -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - CachingConnectionFactory cf1() { - return new CachingConnectionFactory("localhost"); - } - - @Bean - CachingConnectionFactory cf2() { - return new CachingConnectionFactory("otherHost"); - } - - @Bean - CachingConnectionFactory cf3() { - return new CachingConnectionFactory("thirdHost"); - } - - @Bean - SimpleRoutingConnectionFactory rcf(CachingConnectionFactory cf1, - CachingConnectionFactory cf2, CachingConnectionFactory cf3) { - - SimpleRoutingConnectionFactory rcf = new SimpleRoutingConnectionFactory(); - rcf.setDefaultTargetConnectionFactory(cf1); - rcf.setTargetConnectionFactories(Map.of("one", cf1, "two", cf2, "three", cf3)); - return rcf; - } - - @Bean("factory1-admin") - RabbitAdmin admin1(CachingConnectionFactory cf1) { - return new RabbitAdmin(cf1); - } - - @Bean("factory2-admin") - RabbitAdmin admin2(CachingConnectionFactory cf2) { - return new RabbitAdmin(cf2); - } - - @Bean("factory3-admin") - RabbitAdmin admin3(CachingConnectionFactory cf3) { - return new RabbitAdmin(cf3); - } - - @Bean - public RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry() { - return new RabbitListenerEndpointRegistry(); - } - - @Bean - public RabbitListenerAnnotationBeanPostProcessor postProcessor(RabbitListenerEndpointRegistry registry) { - MultiRabbitListenerAnnotationBeanPostProcessor postProcessor - = new MultiRabbitListenerAnnotationBeanPostProcessor(); - postProcessor.setEndpointRegistry(registry); - postProcessor.setContainerFactoryBeanName("defaultContainerFactory"); - return postProcessor; - } - - @Bean - public SimpleRabbitListenerContainerFactory factory1(CachingConnectionFactory cf1) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(cf1); - return factory; - } - - @Bean - public SimpleRabbitListenerContainerFactory factory2(CachingConnectionFactory cf2) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(cf2); - return factory; - } - - @Bean - public SimpleRabbitListenerContainerFactory factory3(CachingConnectionFactory cf3) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(cf3); - return factory; - } - - @Bean - RabbitTemplate template(RoutingConnectionFactory rcf) { - return new RabbitTemplate(rcf); - } - - @Bean - ConnectionFactoryContextWrapper wrapper(SimpleRoutingConnectionFactory rcf) { - return new ConnectionFactoryContextWrapper(rcf); - } - -} - -@Component -class Listeners { - - @RabbitListener(queuesToDeclare = @Queue("q1"), containerFactory = "factory1") - public void listen1(String in) { - - } - - @RabbitListener(queuesToDeclare = @Queue("q2"), containerFactory = "factory2") - public void listen2(String in) { - - } - - @RabbitListener(queuesToDeclare = @Queue("q3"), containerFactory = "factory3") - public void listen3(String in) { - - } - -} ----- -==== - -As you can see, we have declared 3 sets of infrastructure (connection factories, admins, container factories). -As discussed earlier, `@RabbitListener` can define which container factory to use; in this case, they also use `queuesToDeclare` which causes the queue(s) to be declared on the broker, if it doesn't exist. -By naming the `RabbitAdmin` beans with the convention `-admin`, the infrastructure is able to determine which admin should declare the queue. -This will also work with `bindings = @QueueBinding(...)` whereby the exchange and binding will also be declared. -It will NOT work with `queues`, since that expects the queue(s) to already exist. - -On the producer side, a convenient `ConnectionFactoryContextWrapper` class is provided, to make using the `RoutingConnectionFactory` (see <>) simpler. - -As you can see above, a `SimpleRoutingConnectionFactory` bean has been added with routing keys `one`, `two` and `three`. -There is also a `RabbitTemplate` that uses that factory. -Here is an example of using that template with the wrapper to route to one of the broker clusters. - -==== -[source, java] ----- -@Bean -public ApplicationRunner runner(RabbitTemplate template, ConnectionFactoryContextWrapper wrapper) { - return args -> { - wrapper.run("one", () -> template.convertAndSend("q1", "toCluster1")); - wrapper.run("two", () -> template.convertAndSend("q2", "toCluster2")); - wrapper.run("three", () -> template.convertAndSend("q3", "toCluster3")); - }; -} ----- -==== - -==== Debugging - -Spring AMQP provides extensive logging, especially at the `DEBUG` level. - -If you wish to monitor the AMQP protocol between the application and broker, you can use a tool such as WireShark, which has a plugin to decode the protocol. -Alternatively, the RabbitMQ Java client comes with a very useful class called `Tracer`. -When run as a `main`, by default, it listens on port 5673 and connects to port 5672 on localhost. -You can run it and change your connection factory configuration to connect to port 5673 on localhost. -It displays the decoded protocol on the console. -Refer to the `Tracer` Javadoc for more information. diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc deleted file mode 100644 index 859e175f3b..0000000000 --- a/src/reference/asciidoc/appendix.adoc +++ /dev/null @@ -1,1214 +0,0 @@ -[[change-history]] -== Change History - -This section describes what changes have been made as versions have changed. - -=== Current Release - -See <>. - -[[previous-whats-new]] -=== Previous Releases - -==== Changes in 2.3 Since 2.2 - -This section describes the changes between version 2.2 and version 2.3. -See <> for changes in previous versions. - -===== Connection Factory Changes - -Two additional connection factories are now provided. -See <> for more information. - -===== `@RabbitListener` Changes - -You can now specify a reply content type. -See <> for more information. - -===== Message Converter Changes - -The `Jackson2JMessageConverter` s can now deserialize abstract classes (including interfaces) if the `ObjectMapper` is configured with a custom deserializer. -See <> for more information. - -===== Testing Changes - -A new annotation `@SpringRabbitTest` is provided to automatically configure some infrastructure beans for when you are not using `SpringBootTest`. -See <> for more information. - -===== RabbitTemplate Changes - -The template's `ReturnCallback` has been refactored as `ReturnsCallback` for simpler use in lambda expressions. -See <> for more information. - -When using returns and correlated confirms, the `CorrelationData` now requires a unique `id` property. -See <> for more information. - -When using direct reply-to, you can now configure the template such that the server does not need to return correlation data with the reply. -See <> for more information. - -===== Listener Container Changes - -A new listener container property `consumeDelay` is now available; it is helpful when using the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin]. - -The default `JavaLangErrorHandler` now calls `System.exit(99)`. -To revert to the previous behavior (do nothing), add a no-op handler. - -The containers now support the `globalQos` property to apply the `prefetchCount` globally for the channel rather than for each consumer on the channel. - -See <> for more information. - -===== MessagePostProcessor Changes - -The compressing `MessagePostProcessor` s now use a comma to separate multiple content encodings instead of a colon. -The decompressors can handle both formats but, if you produce messages with this version that are consumed by versions earlier than 2.2.12, you should configure the compressor to use the old delimiter. -See the IMPORTANT note in <> for more information. - -===== Multiple Broker Support Improvements - -See <> for more information. - -===== RepublishMessageRecoverer Changes - -A new subclass of this recoverer is not provided that supports publisher confirms. -See <> for more information. - -==== Changes in 2.2 Since 2.1 - -This section describes the changes between version 2.1 and version 2.2. - -===== Package Changes - -The following classes/interfaces have been moved from `org.springframework.amqp.rabbit.core.support` to `org.springframework.amqp.rabbit.batch`: - -* `BatchingStrategy` -* `MessageBatch` -* `SimpleBatchingStrategy` - -In addition, `ListenerExecutionFailedException` has been moved from `org.springframework.amqp.rabbit.listener.exception` to `org.springframework.amqp.rabbit.support`. - -===== Dependency Changes - -JUnit (4) is now an optional dependency and will no longer appear as a transitive dependency. - -The `spring-rabbit-junit` module is now a *compile* dependency in the `spring-rabbit-test` module for a better target application development experience when with only a single `spring-rabbit-test` we get the full stack of testing utilities for AMQP components. - -===== "Breaking" API Changes - -the JUnit (5) `RabbitAvailableCondition.getBrokerRunning()` now returns a `BrokerRunningSupport` instance instead of a `BrokerRunning`, which depends on JUnit 4. -It has the same API so it's just a matter of changing the class name of any references. -See <> for more information. - -===== ListenerContainer Changes - -Messages with fatal exceptions are now rejected and NOT requeued, by default, even if the acknowledge mode is manual. -See <> for more information. - -Listener performance can now be monitored using Micrometer `Timer` s. -See <> for more information. - -===== @RabbitListener Changes - -You can now configure an `executor` on each listener, overriding the factory configuration, to more easily identify threads associated with the listener. -You can now override the container factory's `acknowledgeMode` property with the annotation's `ackMode` property. -See <> for more information. - -When using <>, `@RabbitListener` methods can now receive a complete batch of messages in one call instead of getting them one-at-a-time. - -When receiving batched messages one-at-a-time, the last message has the `isLastInBatch` message property set to true. - -In addition, received batched messages now contain the `amqp_batchSize` header. - -Listeners can also consume batches created in the `SimpleMessageListenerContainer`, even if the batch is not created by the producer. -See <> for more information. - -Spring Data Projection interfaces are now supported by the `Jackson2JsonMessageConverter`. -See <> for more information. - -The `Jackson2JsonMessageConverter` now assumes the content is JSON if there is no `contentType` property, or it is the default (`application/octet-string`). -See <> for more information. - -Similarly. the `Jackson2XmlMessageConverter` now assumes the content is XML if there is no `contentType` property, or it is the default (`application/octet-string`). -See <> for more information. - -When a `@RabbitListener` method returns a result, the bean and `Method` are now available in the reply message properties. -This allows configuration of a `beforeSendReplyMessagePostProcessor` to, for example, set a header in the reply to indicate which method was invoked on the server. -See <> for more information. - -You can now configure a `ReplyPostProcessor` to make modifications to a reply message before it is sent. -See <> for more information. - -===== AMQP Logging Appenders Changes - -The Log4J and Logback `AmqpAppender` s now support a `verifyHostname` SSL option. - -Also these appenders now can be configured to not add MDC entries as headers. -The `addMdcAsHeaders` boolean option has been introduces to configure such a behavior. - -The appenders now support the `SaslConfig` property. - -See <> for more information. - -===== MessageListenerAdapter Changes - -The `MessageListenerAdapter` provides now a new `buildListenerArguments(Object, Channel, Message)` method to build an array of arguments to be passed into target listener and an old one is deprecated. -See <> for more information. - -===== Exchange/Queue Declaration Changes - -The `ExchangeBuilder` and `QueueBuilder` fluent APIs used to create `Exchange` and `Queue` objects for declaration by `RabbitAdmin` now support "well known" arguments. -See <> for more information. - -The `RabbitAdmin` has a new property `explicitDeclarationsOnly`. -See <> for more information. - -===== Connection Factory Changes - -The `CachingConnectionFactory` has a new property `shuffleAddresses`. -When providing a list of broker node addresses, the list will be shuffled before creating a connection so that the order in which the connections are attempted is random. -See <> for more information. - -When using Publisher confirms and returns, the callbacks are now invoked on the connection factory's `executor`. -This avoids a possible deadlock in the `amqp-clients` library if you perform rabbit operations from within the callback. -See <> for more information. - -Also, the publisher confirm type is now specified with the `ConfirmType` enum instead of the two mutually exclusive setter methods. - -The `RabbitConnectionFactoryBean` now uses TLS 1.2 by default when SSL is enabled. -See <> for more information. - -===== New MessagePostProcessor Classes - -Classes `DeflaterPostProcessor` and `InflaterPostProcessor` were added to support compression and decompression, respectively, when the message content-encoding is set to `deflate`. - -===== Other Changes - -The `Declarables` object (for declaring multiple queues, exchanges, bindings) now has a filtered getter for each type. -See <> for more information. - -You can now customize each `Declarable` bean before the `RabbitAdmin` processes the declaration thereof. -See <> for more information. - -`singleActiveConsumer()` has been added to the `QueueBuilder` to set the `x-single-active-consumer` queue argument. -See <> for more information. - -Outbound headers with values of type `Class` are now mapped using `getName()` instead of `toString()`. -See <> for more information. - -Recovery of failed producer-created batches is now supported. -See <> for more information. - -==== Changes in 2.1 Since 2.0 - -===== AMQP Client library - -Spring AMQP now uses the 5.4.x version of the `amqp-client` library provided by the RabbitMQ team. -This client has auto-recovery configured by default. -See <>. - -NOTE: As of version 4.0, the client enables automatic recovery by default. -While compatible with this feature, Spring AMQP has its own recovery mechanisms and the client recovery feature generally is not needed. -We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. -Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. -RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. - - -===== Package Changes - -Certain classes have moved to different packages. -Most are internal classes and do not affect user applications. -Two exceptions are `ChannelAwareMessageListener` and `RabbitListenerErrorHandler`. -These interfaces are now in `org.springframework.amqp.rabbit.listener.api`. - -===== Publisher Confirms Changes - -Channels enabled for publisher confirmations are not returned to the cache while there are outstanding confirmations. -See <> for more information. - -===== Listener Container Factory Improvements - -You can now use the listener container factories to create any listener container, not only those for use with `@RabbitListener` annotations or the `@RabbitListenerEndpointRegistry`. -See <> for more information. - -`ChannelAwareMessageListener` now inherits from `MessageListener`. - -===== Broker Event Listener - -A `BrokerEventListener` is introduced to publish selected broker events as `ApplicationEvent` instances. -See <> for more information. - -===== RabbitAdmin Changes - -The `RabbitAdmin` discovers beans of type `Declarables` (which is a container for `Declarable` - `Queue`, `Exchange`, and `Binding` objects) and declare the contained objects on the broker. -Users are discouraged from using the old mechanism of declaring `>` (and others) and should use `Declarables` beans instead. -By default, the old mechanism is disabled. -See <> for more information. - -`AnonymousQueue` instances are now declared with `x-queue-master-locator` set to `client-local` by default, to ensure the queues are created on the node the application is connected to. -See <> for more information. - -===== RabbitTemplate Changes - -You can now configure the `RabbitTemplate` with the `noLocalReplyConsumer` option to control a `noLocal` flag for reply consumers in the `sendAndReceive()` operations. -See <> for more information. - -`CorrelationData` for publisher confirmations now has a `ListenableFuture`, which you can use to get the acknowledgment instead of using a callback. -When returns and confirmations are enabled, the correlation data, if provided, is populated with the returned message. -See <> for more information. - -A method called `replyTimedOut` is now provided to notify subclasses that a reply has timed out, allowing for any state cleanup. -See <> for more information. - -You can now specify an `ErrorHandler` to be invoked when using request/reply with a `DirectReplyToMessageListenerContainer` (the default) when exceptions occur when replies are delivered (for example, late replies). -See `setReplyErrorHandler` on the `RabbitTemplate`. -(Also since 2.0.11). - -===== Message Conversion - -We introduced a new `Jackson2XmlMessageConverter` to support converting messages from and to XML format. -See <> for more information. - -===== Management REST API - -The `RabbitManagementTemplate` is now deprecated in favor of the direct `com.rabbitmq.http.client.Client` (or `com.rabbitmq.http.client.ReactorNettyClient`) usage. -See <> for more information. - -===== `@RabbitListener` Changes - -The listener container factory can now be configured with a `RetryTemplate` and, optionally, a `RecoveryCallback` used when sending replies. -See <> for more information. - -===== Async `@RabbitListener` Return - -`@RabbitListener` methods can now return `ListenableFuture` or `Mono`. -See <> for more information. - -===== Connection Factory Bean Changes - -By default, the `RabbitConnectionFactoryBean` now calls `enableHostnameVerification()`. -To revert to the previous behavior, set the `enableHostnameVerification` property to `false`. - -===== Connection Factory Changes - -The `CachingConnectionFactory` now unconditionally disables auto-recovery in the underlying RabbitMQ `ConnectionFactory`, even if a pre-configured instance is provided in a constructor. -While steps have been taken to make Spring AMQP compatible with auto recovery, certain corner cases have arisen where issues remain. -Spring AMQP has had its own recovery mechanism since 1.0.0 and does not need to use the recovery provided by the client. -While it is still possible to enable the feature (using `cachingConnectionFactory.getRabbitConnectionFactory()` `.setAutomaticRecoveryEnabled()`) after the `CachingConnectionFactory` is constructed, **we strongly recommend that you not do so**. -We recommend that you use a separate RabbitMQ `ConnectionFactory` if you need auto recovery connections when using the client factory directly (rather than using Spring AMQP components). - -===== Listener Container Changes - -The default `ConditionalRejectingErrorHandler` now completely discards messages that cause fatal errors if an `x-death` header is present. -See <> for more information. - -===== Immediate requeue - -A new `ImmediateRequeueAmqpException` is introduced to notify a listener container that the message has to be re-queued. -To use this feature, a new `ImmediateRequeueMessageRecoverer` implementation is added. - -See <> for more information. - - -==== Changes in 2.0 Since 1.7 - -===== Using `CachingConnectionFactory` - -Starting with version 2.0.2, you can configure the `RabbitTemplate` to use a different connection to that used by listener containers. -This change avoids deadlocked consumers when producers are blocked for any reason. -See <> for more information. - -===== AMQP Client library - -Spring AMQP now uses the new 5.0.x version of the `amqp-client` library provided by the RabbitMQ team. -This client has auto recovery configured by default. -See <>. - -NOTE: As of version 4.0, the client enables automatic recovery by default. -While compatible with this feature, Spring AMQP has its own recovery mechanisms, and the client recovery feature generally is not needed. -We recommend that you disable `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. -Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. -RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. - -===== General Changes - -The `ExchangeBuilder` now builds durable exchanges by default. -The `@Exchange` annotation used within a `@QeueueBinding` also declares durable exchanges by default. -The `@Queue` annotation used within a `@RabbitListener` by default declares durable queues if named and non-durable if anonymous. -See <> and <> for more information. - -===== Deleted Classes - -`UniquelyNameQueue` is no longer provided. -It is unusual to create a durable non-auto-delete queue with a unique name. -This class has been deleted. -If you require its functionality, use `new Queue(UUID.randomUUID().toString())`. - -===== New Listener Container - -The `DirectMessageListenerContainer` has been added alongside the existing `SimpleMessageListenerContainer`. -See <> and <> for information about choosing which container to use as well as how to configure them. - - -===== Log4j Appender - -This appender is no longer available due to the end-of-life of log4j. -See <> for information about the available log appenders. - - -===== `RabbitTemplate` Changes - -IMPORTANT: Previously, a non-transactional `RabbitTemplate` participated in an existing transaction if it ran on a transactional listener container thread. -This was a serious bug. -However, users might have relied on this behavior. -Starting with version 1.6.2, you must set the `channelTransacted` boolean on the template for it to participate in the container transaction. - -The `RabbitTemplate` now uses a `DirectReplyToMessageListenerContainer` (by default) instead of creating a new consumer for each request. -See <> for more information. - -The `AsyncRabbitTemplate` now supports direct reply-to. -See <> for more information. - -The `RabbitTemplate` and `AsyncRabbitTemplate` now have `receiveAndConvert` and `convertSendAndReceiveAsType` methods that take a `ParameterizedTypeReference` argument, letting the caller specify the type to which to convert the result. -This is particularly useful for complex types or when type information is not conveyed in message headers. -It requires a `SmartMessageConverter` such as the `Jackson2JsonMessageConverter`. -See <>, <>, <>, and <> for more information. - -You can now use a `RabbitTemplate` to perform multiple operations on a dedicated channel. -See <> for more information. - -===== Listener Adapter - -A convenient `FunctionalInterface` is available for using lambdas with the `MessageListenerAdapter`. -See <> for more information. - -===== Listener Container Changes - -====== Prefetch Default Value - -The prefetch default value used to be 1, which could lead to under-utilization of efficient consumers. -The default prefetch value is now 250, which should keep consumers busy in most common scenarios and, -thus, improve throughput. - -IMPORTANT: There are scenarios where the prefetch value should -be low -- for example, with large messages, especially if the processing is slow (messages could add up -to a large amount of memory in the client process), and if strict message ordering is necessary -(the prefetch value should be set back to 1 in this case). -Also, with low-volume messaging and multiple consumers (including concurrency within a single listener container instance), you may wish to reduce the prefetch to get a more even distribution of messages across consumers. - -For more background about prefetch, see this post about https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/[consumer utilization in RabbitMQ] -and this post about https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/[queuing theory]. - -====== Message Count - -Previously, `MessageProperties.getMessageCount()` returned `0` for messages emitted by the container. -This property applies only when you use `basicGet` (for example, from `RabbitTemplate.receive()` methods) and is now initialized to `null` for container messages. - -====== Transaction Rollback Behavior - -Message re-queue on transaction rollback is now consistent, regardless of whether or not a transaction manager is configured. -See <> for more information. - -====== Shutdown Behavior - -If the container threads do not respond to a shutdown within `shutdownTimeout`, the channels are forced closed by default. -See <> for more information. - -====== After Receive Message Post Processors - -If a `MessagePostProcessor` in the `afterReceiveMessagePostProcessors` property returns `null`, the message is discarded (and acknowledged if appropriate). - -===== Connection Factory Changes - -The connection and channel listener interfaces now provide a mechanism to obtain information about exceptions. -See <> and <> for more information. - -A new `ConnectionNameStrategy` is now provided to populate the application-specific identification of the target RabbitMQ connection from the `AbstractConnectionFactory`. -See <> for more information. - -===== Retry Changes - -The `MissingMessageIdAdvice` is no longer provided. -Its functionality is now built-in. -See <> for more information. - -===== Anonymous Queue Naming - -By default, `AnonymousQueues` are now named with the default `Base64UrlNamingStrategy` instead of a simple `UUID` string. -See <> for more information. - -===== `@RabbitListener` Changes - -You can now provide simple queue declarations (bound only to the default exchange) in `@RabbitListener` annotations. -See <> for more information. - -You can now configure `@RabbitListener` annotations so that any exceptions are returned to the sender. -You can also configure a `RabbitListenerErrorHandler` to handle exceptions. -See <> for more information. - -You can now bind a queue with multiple routing keys when you use the `@QueueBinding` annotation. -Also `@QueueBinding.exchange()` now supports custom exchange types and declares durable exchanges by default. - -You can now set the `concurrency` of the listener container at the annotation level rather than having to configure a different container factory for different concurrency settings. - -You can now set the `autoStartup` property of the listener container at the annotation level, overriding the default setting in the container factory. - -You can now set after receive and before send (reply) `MessagePostProcessor` instances in the `RabbitListener` container factories. - -See <> for more information. - -Starting with version 2.0.3, one of the `@RabbitHandler` annotations on a class-level `@RabbitListener` can be designated as the default. -See <> for more information. - -===== Container Conditional Rollback - -When using an external transaction manager (such as JDBC), rule-based rollback is now supported when you provide the container with a transaction attribute. -It is also now more flexible when you use a transaction advice. -See <> for more information. - -===== Remove Jackson 1.x support - -Deprecated in previous versions, Jackson `1.x` converters and related components have now been deleted. -You can use similar components based on Jackson 2.x. -See <> for more information. - -===== JSON Message Converter - -When the `__TypeId__` is set to `Hashtable` for an inbound JSON message, the default conversion type is now `LinkedHashMap`. -Previously, it was `Hashtable`. -To revert to a `Hashtable`, you can use `setDefaultMapType` on the `DefaultClassMapper`. - -===== XML Parsers - -When parsing `Queue` and `Exchange` XML components, the parsers no longer register the `name` attribute value as a bean alias if an `id` attribute is present. -See <> for more information. - -===== Blocked Connection -You can now inject the `com.rabbitmq.client.BlockedListener` into the `org.springframework.amqp.rabbit.connection.Connection` object. -Also, the `ConnectionBlockedEvent` and `ConnectionUnblockedEvent` events are emitted by the `ConnectionFactory` when the connection is blocked or unblocked by the Broker. - -See <> for more information. - -==== Changes in 1.7 Since 1.6 - -===== AMQP Client library - -Spring AMQP now uses the new 4.0.x version of the `amqp-client` library provided by the RabbitMQ team. -This client has auto-recovery configured by default. -See <>. - -NOTE: The 4.0.x client enables automatic recovery by default. -While compatible with this feature, Spring AMQP has its own recovery mechanisms, and the client recovery feature generally is not needed. -We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. -Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. -RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. - - -===== Log4j 2 upgrade -The minimum Log4j 2 version (for the `AmqpAppender`) is now `2.7`. -The framework is no longer compatible with previous versions. -See <> for more information. - -===== Logback Appender - -This appender no longer captures caller data (method, line number) by default. -You can re-enable it by setting the `includeCallerData` configuration option. -See <> for information about the available log appenders. - -===== Spring Retry Upgrade - -The minimum Spring Retry version is now `1.2`. -The framework is no longer compatible with previous versions. - -====== Shutdown Behavior - -You can now set `forceCloseChannel` to `true` so that, if the container threads do not respond to a shutdown within `shutdownTimeout`, the channels are forced closed, -causing any unacked messages to be re-queued. -See <> for more information. - -===== FasterXML Jackson upgrade - -The minimum Jackson version is now `2.8`. -The framework is no longer compatible with previous versions. - -===== JUnit `@Rules` - -Rules that have previously been used internally by the framework have now been made available in a separate jar called `spring-rabbit-junit`. -See <> for more information. - -===== Container Conditional Rollback - -When you use an external transaction manager (such as JDBC), rule-based rollback is now supported when you provide the container with a transaction attribute. -It is also now more flexible when you use a transaction advice. - -===== Connection Naming Strategy - -A new `ConnectionNameStrategy` is now provided to populate the application-specific identification of the target RabbitMQ connection from the `AbstractConnectionFactory`. -See <> for more information. - -===== Listener Container Changes - -====== Transaction Rollback Behavior - -You can now configure message re-queue on transaction rollback to be consistent, regardless of whether or not a transaction manager is configured. -See <> for more information. - -==== Earlier Releases - -See <> for changes in previous versions. - -==== Changes in 1.6 Since 1.5 - -===== Testing Support - -A new testing support library is now provided. -See <> for more information. - -===== Builder - -Builders that provide a fluent API for configuring `Queue` and `Exchange` objects are now available. -See <> for more information. - -===== Namespace Changes - -====== Connection Factory - -You can now add a `thread-factory` to a connection factory bean declaration -- for example, to name the threads -created by the `amqp-client` library. -See <> for more information. - -When you use `CacheMode.CONNECTION`, you can now limit the total number of connections allowed. -See <> for more information. - -====== Queue Definitions - -You can now provide a naming strategy for anonymous queues. -See <> for more information. - -===== Listener Container Changes - -====== Idle Message Listener Detection - -You can now configure listener containers to publish `ApplicationEvent` instances when idle. -See <> for more information. - -====== Mismatched Queue Detection - -By default, when a listener container starts, if queues with mismatched properties or arguments are detected, -the container logs the exception but continues to listen. -The container now has a property called `mismatchedQueuesFatal`, which prevents the container (and context) from -starting if the problem is detected during startup. -It also stops the container if the problem is detected later, such as after recovering from a connection failure. -See <> for more information. - -====== Listener Container Logging - -Now, listener container provides its `beanName` to the internal `SimpleAsyncTaskExecutor` as a `threadNamePrefix`. -It is useful for logs analysis. - -====== Default Error Handler - -The default error handler (`ConditionalRejectingErrorHandler`) now considers irrecoverable `@RabbitListener` -exceptions as fatal. -See <> for more information. - - -===== `AutoDeclare` and `RabbitAdmin` Instances - -See <> (`autoDeclare`) for some changes to the semantics of that option with respect to the use -of `RabbitAdmin` instances in the application context. - -===== `AmqpTemplate`: Receive with Timeout - -A number of new `receive()` methods with `timeout` have been introduced for the `AmqpTemplate` -and its `RabbitTemplate` implementation. -See <> for more information. - -===== Using `AsyncRabbitTemplate` - -A new `AsyncRabbitTemplate` has been introduced. -This template provides a number of send and receive methods, where the return value is a `ListenableFuture`, which can -be used later to obtain the result either synchronously or asynchronously. -See <> for more information. - -===== `RabbitTemplate` Changes - -1.4.1 introduced the ability to use https://www.rabbitmq.com/direct-reply-to.html[direct reply-to] when the broker supports it. -It is more efficient than using a temporary queue for each reply. -This version lets you override this default behavior and use a temporary queue by setting the `useTemporaryReplyQueues` property to `true`. -See <> for more information. - -The `RabbitTemplate` now supports a `user-id-expression` (`userIdExpression` when using Java configuration). -See https://www.rabbitmq.com/validated-user-id.html[Validated User-ID RabbitMQ documentation] and <> for more information. - -===== Message Properties - -====== Using `CorrelationId` - -The `correlationId` message property can now be a `String`. -See <> for more information. - -====== Long String Headers - -Previously, the `DefaultMessagePropertiesConverter` "`converted`" headers longer than the long string limit (default 1024) -to a `DataInputStream` (actually, it referenced the `LongString` instance's `DataInputStream`). -On output, this header was not converted (except to a String -- for example, `java.io.DataInputStream@1d057a39` by calling -`toString()` on the stream). - -With this release, long `LongString` instances are now left as `LongString` instances by default. -You can access the contents by using the `getBytes[]`, `toString()`, or `getStream()` methods. -A large incoming `LongString` is now correctly "`converted`" on output too. - -See <> for more information. - -====== Inbound Delivery Mode - -The `deliveryMode` property is no longer mapped to the `MessageProperties.deliveryMode`. -This change avoids unintended propagation if the the same `MessageProperties` object is used to send an outbound message. -Instead, the inbound `deliveryMode` header is mapped to `MessageProperties.receivedDeliveryMode`. - -See <> for more information. - -When using annotated endpoints, the header is provided in the header named `AmqpHeaders.RECEIVED_DELIVERY_MODE`. - -See <> for more information. - -====== Inbound User ID - -The `user_id` property is no longer mapped to the `MessageProperties.userId`. -This change avoids unintended propagation if the the same `MessageProperties` object is used to send an outbound message. -Instead, the inbound `userId` header is mapped to `MessageProperties.receivedUserId`. - -See <> for more information. - -When you use annotated endpoints, the header is provided in the header named `AmqpHeaders.RECEIVED_USER_ID`. - -See <> for more information. - -===== `RabbitAdmin` Changes - -====== Declaration Failures - -Previously, the `ignoreDeclarationFailures` flag took effect only for `IOException` on the channel (such as mis-matched -arguments). -It now takes effect for any exception (such as `TimeoutException`). -In addition, a `DeclarationExceptionEvent` is now published whenever a declaration fails. -The `RabbitAdmin` last declaration event is also available as a property `lastDeclarationExceptionEvent`. -See <> for more information. - -===== `@RabbitListener` Changes - -====== Multiple Containers for Each Bean - -When you use Java 8 or later, you can now add multiple `@RabbitListener` annotations to `@Bean` classes or -their methods. -When using Java 7 or earlier, you can use the `@RabbitListeners` container annotation to provide the same -functionality. -See <> for more information. - -====== `@SendTo` SpEL Expressions - -`@SendTo` for routing replies with no `replyTo` property can now be SpEL expressions evaluated against the -request/reply. -See <> for more information. - -====== `@QueueBinding` Improvements - -You can now specify arguments for queues, exchanges, and bindings in `@QueueBinding` annotations. -Header exchanges are now supported by `@QueueBinding`. -See <> for more information. - -===== Delayed Message Exchange - -Spring AMQP now has first class support for the RabbitMQ Delayed Message Exchange plugin. -See <> for more information. - -===== Exchange Internal Flag - -Any `Exchange` definitions can now be marked as `internal`, and `RabbitAdmin` passes the value to the broker when -declaring the exchange. -See <> for more information. - -===== `CachingConnectionFactory` Changes - -====== `CachingConnectionFactory` Cache Statistics - -The `CachingConnectionFactory` now provides cache properties at runtime and over JMX. -See <> for more information. - -====== Accessing the Underlying RabbitMQ Connection Factory - -A new getter has been added to provide access to the underlying factory. -You can use this getter, for example, to add custom connection properties. -See <> for more information. - -====== Channel Cache - -The default channel cache size has been increased from 1 to 25. -See <> for more information. - -In addition, the `SimpleMessageListenerContainer` no longer adjusts the cache size to be at least as large as the number -of `concurrentConsumers` -- this was superfluous, since the container consumer channels are never cached. - -===== Using `RabbitConnectionFactoryBean` - -The factory bean now exposes a property to add client connection properties to connections made by the resulting -factory. - -===== Java Deserialization - -You can now configure a "`allowed list`" of allowable classes when you use Java deserialization. -You should consider creating an allowed list if you accept messages with serialized java objects from -untrusted sources. -See <> for more information. - -===== JSON `MessageConverter` - -Improvements to the JSON message converter now allow the consumption of messages that do not have type information -in message headers. -See <> and <> for more information. - -===== Logging Appenders - -====== Log4j 2 - -A log4j 2 appender has been added, and the appenders can now be configured with an `addresses` property to connect -to a broker cluster. - -====== Client Connection Properties - -You can now add custom client connection properties to RabbitMQ connections. - -See <> for more information. - -==== Changes in 1.5 Since 1.4 - -===== `spring-erlang` Is No Longer Supported - -The `spring-erlang` jar is no longer included in the distribution. -Use <> instead. - -===== `CachingConnectionFactory` Changes - -====== Empty Addresses Property in `CachingConnectionFactory` - -Previously, if the connection factory was configured with a host and port but an empty String was also supplied for -`addresses`, the host and port were ignored. -Now, an empty `addresses` String is treated the same as a `null`, and the host and port are used. - -====== URI Constructor - -The `CachingConnectionFactory` has an additional constructor, with a `URI` parameter, to configure the broker connection. - -====== Connection Reset - -A new method called `resetConnection()` has been added to let users reset the connection (or connections). -You might use this, for example, to reconnect to the primary broker after failing over to the secondary broker. -This *does* impact in-process operations. -The existing `destroy()` method does exactly the same, but the new method has a less daunting name. - -===== Properties to Control Container Queue Declaration Behavior - -When the listener container consumers start, they attempt to passively declare the queues to ensure they are available -on the broker. -Previously, if these declarations failed (for example, because the queues didn't exist) or when an HA queue was being -moved, the retry logic was fixed at three retry attempts at five-second intervals. -If the queues still do not exist, the behavior is controlled by the `missingQueuesFatal` property (default: `true`). -Also, for containers configured to listen from multiple queues, if only a subset of queues are available, the consumer -retried the missing queues on a fixed interval of 60 seconds. - -The `declarationRetries`, `failedDeclarationRetryInterval`, and `retryDeclarationInterval` properties are now configurable. -See <> for more information. - -===== Class Package Change - -The `RabbitGatewaySupport` class has been moved from `o.s.amqp.rabbit.core.support` to `o.s.amqp.rabbit.core`. - -===== `DefaultMessagePropertiesConverter` Changes - -You can now configure the `DefaultMessagePropertiesConverter` to -determine the maximum length of a `LongString` that is converted -to a `String` rather than to a `DataInputStream`. -The converter has an alternative constructor that takes the value as a limit. -Previously, this limit was hard-coded at `1024` bytes. -(Also available in 1.4.4). - -===== `@RabbitListener` Improvements - -====== `@QueueBinding` for `@RabbitListener` - -The `bindings` attribute has been added to the `@RabbitListener` annotation as mutually exclusive with the `queues` -attribute to allow the specification of the `queue`, its `exchange`, and `binding` for declaration by a `RabbitAdmin` on -the Broker. - -====== SpEL in `@SendTo` - -The default reply address (`@SendTo`) for a `@RabbitListener` can now be a SpEL expression. - -====== Multiple Queue Names through Properties - -You can now use a combination of SpEL and property placeholders to specify multiple queues for a listener. - -See <> for more information. - -===== Automatic Exchange, Queue, and Binding Declaration - -You can now declare beans that define a collection of these entities, and the `RabbitAdmin` adds the -contents to the list of entities that it declares when a connection is established. -See <> for more information. - -===== `RabbitTemplate` Changes - -====== `reply-address` Added - -The `reply-address` attribute has been added to the `` component as an alternative `reply-queue`. -See <> for more information. -(Also available in 1.4.4 as a setter on the `RabbitTemplate`). - -====== Blocking `receive` Methods - -The `RabbitTemplate` now supports blocking in `receive` and `convertAndReceive` methods. -See <> for more information. - -====== Mandatory with `sendAndReceive` Methods - -When the `mandatory` flag is set when using the `sendAndReceive` and `convertSendAndReceive` methods, the calling thread -throws an `AmqpMessageReturnedException` if the request message cannot be deliverted. -See <> for more information. - -====== Improper Reply Listener Configuration - -The framework tries to verify proper configuration of a reply listener container when using a named reply queue. - -See <> for more information. - -===== `RabbitManagementTemplate` Added - -The `RabbitManagementTemplate` has been introduced to monitor and configure the RabbitMQ Broker by using the REST API provided by its https://www.rabbitmq.com/management.html[management plugin]. -See <> for more information. - -===== Listener Container Bean Names (XML) - -[IMPORTANT] -==== -The `id` attribute on the `` element has been removed. -Starting with this release, the `id` on the `` child element is used alone to name the listener container bean created for each listener element. - -Normal Spring bean name overrides are applied. -If a later `` is parsed with the same `id` as an existing bean, the new definition overrides the existing one. -Previously, bean names were composed from the `id` attributes of the `` and `` elements. - -When migrating to this release, if you have `id` attributes on your `` elements, remove them and set the `id` on the child `` element instead. -==== - -However, to support starting and stopping containers as a group, a new `group` attribute has been added. -When this attribute is defined, the containers created by this element are added to a bean with this name, of type `Collection`. -You can iterate over this group to start and stop containers. - -===== Class-Level `@RabbitListener` - -The `@RabbitListener` annotation can now be applied at the class level. -Together with the new `@RabbitHandler` method annotation, this lets you select the handler method based on payload type. -See <> for more information. - -===== `SimpleMessageListenerContainer`: BackOff Support - -The `SimpleMessageListenerContainer` can now be supplied with a `BackOff` instance for `consumer` startup recovery. -See <> for more information. - -===== Channel Close Logging - -A mechanism to control the log levels of channel closure has been introduced. -See <>. - -===== Application Events - -The `SimpleMessageListenerContainer` now emits application events when consumers fail. -See <> for more information. - -===== Consumer Tag Configuration - -Previously, the consumer tags for asynchronous consumers were generated by the broker. -With this release, it is now possible to supply a naming strategy to the listener container. -See <>. - -===== Using `MessageListenerAdapter` - -The `MessageListenerAdapter` now supports a map of queue names (or consumer tags) to method names, to determine -which delegate method to call based on the queue from which the message was received. - -===== `LocalizedQueueConnectionFactory` Added - -`LocalizedQueueConnectionFactory` is a new connection factory that connects to the node in a cluster where a mirrored queue actually resides. - -See <>. - -===== Anonymous Queue Naming - -Starting with version 1.5.3, you can now control how `AnonymousQueue` names are generated. -See <> for more information. - - -==== Changes in 1.4 Since 1.3 - -===== `@RabbitListener` Annotation - -POJO listeners can be annotated with `@RabbitListener`, enabled by `@EnableRabbit` or ``. -Spring Framework 4.1 is required for this feature. -See <> for more information. - -===== `RabbitMessagingTemplate` Added - -A new `RabbitMessagingTemplate` lets you interact with RabbitMQ by using `spring-messaging` `Message` instances. -Internally, it uses the `RabbitTemplate`, which you can configure as normal. -Spring Framework 4.1 is required for this feature. -See <> for more information. - -===== Listener Container `missingQueuesFatal` Attribute - -1.3.5 introduced the `missingQueuesFatal` property on the `SimpleMessageListenerContainer`. -This is now available on the listener container namespace element. -See <>. - -===== RabbitTemplate `ConfirmCallback` Interface - -The `confirm` method on this interface has an additional parameter called `cause`. -When available, this parameter contains the reason for a negative acknowledgement (nack). -See <>. - -===== `RabbitConnectionFactoryBean` Added - -`RabbitConnectionFactoryBean` creates the underlying RabbitMQ `ConnectionFactory` used by the `CachingConnectionFactory`. -This enables configuration of SSL options using Spring's dependency injection. -See <>. - -===== Using `CachingConnectionFactory` - -The `CachingConnectionFactory` now lets the `connectionTimeout` be set as a property or as an attribute in the namespace. -It sets the property on the underlying RabbitMQ `ConnectionFactory`. -See <>. - -===== Log Appender - -The Logback `org.springframework.amqp.rabbit.logback.AmqpAppender` has been introduced. -It provides options similar to `org.springframework.amqp.rabbit.log4j.AmqpAppender`. -For more information, see the JavaDoc of these classes. - -The Log4j `AmqpAppender` now supports the `deliveryMode` property (`PERSISTENT` or `NON_PERSISTENT`, default: `PERSISTENT`). -Previously, all log4j messages were `PERSISTENT`. - -The appender also supports modification of the `Message` before sending -- allowing, for example, the addition of custom headers. -Subclasses should override the `postProcessMessageBeforeSend()`. - -===== Listener Queues - -The listener container now, by default, redeclares any missing queues during startup. -A new `auto-declare` attribute has been added to the `` to prevent these re-declarations. -See <>. - -===== `RabbitTemplate`: `mandatory` and `connectionFactorySelector` Expressions - -The `mandatoryExpression`, `sendConnectionFactorySelectorExpression`, and `receiveConnectionFactorySelectorExpression` SpEL Expression`s properties have been added to `RabbitTemplate`. -The `mandatoryExpression` is used to evaluate a `mandatory` boolean value against each request message when a `ReturnCallback` is in use. -See <>. -The `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` are used when an `AbstractRoutingConnectionFactory` is provided, to determine the `lookupKey` for the target `ConnectionFactory` at runtime on each AMQP protocol interaction operation. -See <>. - -===== Listeners and the Routing Connection Factory - -You can configure a `SimpleMessageListenerContainer` with a routing connection factory to enable connection selection based on the queue names. -See <>. - -===== `RabbitTemplate`: `RecoveryCallback` Option - -The `recoveryCallback` property has been added for use in the `retryTemplate.execute()`. -See <>. - -===== `MessageConversionException` Change - -This exception is now a subclass of `AmqpException`. -Consider the following code: - -==== -[source,java] ----- -try { - template.convertAndSend("thing1", "thing2", "cat"); -} -catch (AmqpException e) { - ... -} -catch (MessageConversionException e) { - ... -} ----- -==== - -The second catch block is no longer reachable and needs to be moved above the catch-all `AmqpException` catch block. - -===== RabbitMQ 3.4 Compatibility - -Spring AMQP is now compatible with the RabbitMQ 3.4, including direct reply-to. -See <> and <> for more information. - -===== `ContentTypeDelegatingMessageConverter` Added - -The `ContentTypeDelegatingMessageConverter` has been introduced to select the `MessageConverter` to use, based on the `contentType` property in the `MessageProperties`. -See <> for more information. - -==== Changes in 1.3 Since 1.2 - -===== Listener Concurrency - -The listener container now supports dynamic scaling of the number of consumers based on workload, or you can programmatically change the concurrency without stopping the container. -See <>. - -===== Listener Queues - -The listener container now permits the queues on which it listens to be modified at runtime. -Also, the container now starts if at least one of its configured queues is available for use. -See <> - -This listener container now redeclares any auto-delete queues during startup. -See <>. - -===== Consumer Priority - -The listener container now supports consumer arguments, letting the `x-priority` argument be set. -See <>. - -===== Exclusive Consumer - -You can now configure `SimpleMessageListenerContainer` with a single `exclusive` consumer, preventing other consumers from listening to the queue. -See <>. - -===== Rabbit Admin - -You can now have the broker generate the queue name, regardless of `durable`, `autoDelete`, and `exclusive` settings. -See <>. - -===== Direct Exchange Binding - -Previously, omitting the `key` attribute from a `binding` element of a `direct-exchange` configuration caused the queue or exchange to be bound with an empty string as the routing key. -Now it is bound with the the name of the provided `Queue` or `Exchange`. -If you wish to bind with an empty string routing key, you need to specify `key=""`. - -===== `AmqpTemplate` Changes - -The `AmqpTemplate` now provides several synchronous `receiveAndReply` methods. -These are implemented by the `RabbitTemplate`. -For more information see <>. - -The `RabbitTemplate` now supports configuring a `RetryTemplate` to attempt retries (with optional back-off policy) for when the broker is not available. -For more information see <>. - -===== Caching Connection Factory - -You can now configure the caching connection factory to cache `Connection` instances and their `Channel` instances instead of using a single connection and caching only `Channel` instances. -See <>. - -===== Binding Arguments - -The `` of the `` now supports parsing of the `` sub-element. -You can now configure the `` of the `` with a `key/value` attribute pair (to match on a single header) or with a `` sub-element (allowing matching on multiple headers). -These options are mutually exclusive. -See <>. - -===== Routing Connection Factory - -A new `SimpleRoutingConnectionFactory` has been introduced. -It allows configuration of `ConnectionFactories` mapping, to determine the target `ConnectionFactory` to use at runtime. -See <>. - -===== `MessageBuilder` and `MessagePropertiesBuilder` - -"`Fluent APIs`" for building messages or message properties are now provided. -See <>. - -===== `RetryInterceptorBuilder` Change - -A "`Fluent API`" for building listener container retry interceptors is now provided. -See <>. - -===== `RepublishMessageRecoverer` Added - -This new `MessageRecoverer` is provided to allow publishing a failed message to another queue (including stack trace information in the header) when retries are exhausted. -See <>. - -===== Default Error Handler (Since 1.3.2) - -A default `ConditionalRejectingErrorHandler` has been added to the listener container. -This error handler detects fatal message conversion problems and instructs the container to reject the message to prevent the broker from continually redelivering the unconvertible message. -See <>. - -===== Listener Container 'missingQueuesFatal` Property (Since 1.3.5) - -The `SimpleMessageListenerContainer` now has a property called `missingQueuesFatal` (default: `true`). -Previously, missing queues were always fatal. -See <>. - -==== Changes to 1.2 Since 1.1 - -===== RabbitMQ Version - -Spring AMQP now uses RabbitMQ 3.1.x by default (but retains compatibility with earlier versions). -Certain deprecations have been added for features no longer supported by RabbitMQ 3.1.x -- federated exchanges and the `immediate` property on the `RabbitTemplate`. - -===== Rabbit Admin - -`RabbitAdmin` now provides an option to let exchange, queue, and binding declarations continue when a declaration fails. -Previously, all declarations stopped on a failure. -By setting `ignore-declaration-exceptions`, such exceptions are logged (at the `WARN` level), but further declarations continue. -An example where this might be useful is when a queue declaration fails because of a slightly different `ttl` setting that would normally stop other declarations from proceeding. - -`RabbitAdmin` now provides an additional method called `getQueueProperties()`. -You can use this determine if a queue exists on the broker (returns `null` for a non-existent queue). -In addition, it returns the current number of messages in the queue as well as the current number of consumers. - -===== Rabbit Template - -Previously, when the `...sendAndReceive()` methods were used with a fixed reply queue, two custom headers were used for correlation data and to retain and restore reply queue information. -With this release, the standard message property (`correlationId`) is used by default, although you can specify a custom property to use instead. -In addition, nested `replyTo` information is now retained internally in the template, instead of using a custom header. - -The `immediate` property is deprecated. -You must not set this property when using RabbitMQ 3.0.x or greater. - -===== JSON Message Converters - -A Jackson 2.x `MessageConverter` is now provided, along with the existing converter that uses Jackson 1.x. - -===== Automatic Declaration of Queues and Other Items - -Previously, when declaring queues, exchanges and bindings, you could not define which connection factory was used for the declarations. -Each `RabbitAdmin` declared all components by using its connection. - -Starting with this release, you can now limit declarations to specific `RabbitAdmin` instances. -See <>. - -===== AMQP Remoting - -Facilities are now provided for using Spring remoting techniques, using AMQP as the transport for the RPC calls. -For more information see <> - -===== Requested Heart Beats - -Several users have asked for the underlying client connection factory's `requestedHeartBeats` property to be exposed on the Spring AMQP `CachingConnectionFactory`. -This is now available. -Previously, it was necessary to configure the AMQP client factory as a separate bean and provide a reference to it in the `CachingConnectionFactory`. - -==== Changes to 1.1 Since 1.0 - -===== General - -Spring-AMQP is now built with Gradle. - -Adds support for publisher confirms and returns. - -Adds support for HA queues and broker failover. - -Adds support for dead letter exchanges and dead letter queues. - -===== AMQP Log4j Appender - -Adds an option to support adding a message ID to logged messages. - -Adds an option to allow the specification of a `Charset` name to be used when converting `String` to `byte[]`. diff --git a/src/reference/asciidoc/docinfo.html b/src/reference/asciidoc/docinfo.html deleted file mode 100644 index 19e2462b2c..0000000000 --- a/src/reference/asciidoc/docinfo.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/src/reference/asciidoc/index.adoc b/src/reference/asciidoc/index.adoc deleted file mode 100644 index 3fdc7429e6..0000000000 --- a/src/reference/asciidoc/index.adoc +++ /dev/null @@ -1,69 +0,0 @@ -[[spring-amqp-reference]] -= Spring AMQP -ifdef::backend-html5[] -:revnumber: '' -endif::[] -:toc: left -:toclevels: 4 -:numbered: -:icons: font -:hide-uri-scheme: -Mark Pollack; Mark Fisher; Oleg Zhurakousky; Dave Syer; Gary Russell; Gunnar Hillert; Artem Bilan; Stéphane Nicoll; Arnaud Cogoluègnes; Jay Bryant - -ifdef::backend-html5[] -*{project-version}* - -NOTE: This documentation is also available as https://docs.spring.io/spring-amqp/docs/current/reference/pdf/spring-amqp-reference.pdf[PDF]. -endif::[] - -ifdef::backend-pdf[] -NOTE: This documentation is also available as https://docs.spring.io/spring-amqp/docs/current/reference/html/index.html[HTML]. -endif::[] - -(C) 2010 - 2021 by VMware, Inc. - -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. - - -== Preface - -include::preface.adoc[] - -include::whats-new.adoc[] - -== Introduction - -This first part of the reference documentation is a high-level overview of Spring AMQP and the underlying concepts. -It includes some code snippets to get you up and running as quickly as possible. - -include::quick-tour.adoc[] - -== Reference - -This part of the reference documentation details the various components that comprise Spring AMQP. -The <> covers the core classes to develop an AMQP application. -This part also includes a chapter about the <>. - -include::amqp.adoc[] - -include::logging.adoc[] - -include::sample-apps.adoc[] - -include::testing.adoc[] - -== Spring Integration - Reference - -This part of the reference documentation provides a quick introduction to the AMQP support within the Spring Integration project. - -include::si-amqp.adoc[] - -[[resources]] -== Other Resources - -In addition to this reference documentation, there exist a number of other resources that may help you learn about AMQP. - -include::further-reading.adoc[] - -[appendix] -include::appendix.adoc[] diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc deleted file mode 100644 index f625e8a44e..0000000000 --- a/src/reference/asciidoc/whats-new.adoc +++ /dev/null @@ -1,12 +0,0 @@ -[[whats-new]] -== What's New - -=== Changes in 2.4 Since 2.3 - -This section describes the changes between version 2.4 and version 2.4. -See <> for changes in previous versions. - -==== @RabbitListener Changes - -`MessageProperties` is now available for argument matching. -See <> for more information.